From e695da66df128069c376f6c2ffd4b70e7be49c72 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:57:55 -0500 Subject: [PATCH 01/75] GCodeWriter: extract MachineKinematics, delete BeltGCodeWriter BeltGCodeWriter subclassed GCodeWriter and overrode seven methods, five of them by copying the base body and changing the transform. The base writer already carried an axis remap and already branched at each of its seven coordinate-emission decisions; the subclass did the same branching with a different transform, and the two copies had begun to drift. Replace the inheritance with a strategy object owned by GCodeWriter: CartesianKinematics to_machine = the existing apply_axis_remap; today's base behaviour, moved rather than changed. BeltKinematics to_machine = MachineFrameTransform o axis_remap o BeltBackTransform, plus a world_coordinates variant for the PA calibration generators. New: src/libslic3r/GCode/MachineKinematics.{hpp,cpp}, GCode/BeltKinematics.{hpp,cpp} Deleted: src/libslic3r/BeltGCodeWriter.{hpp,cpp} (341 lines) Points worth a reviewer's attention: * The predicate is must_emit_all_axes(), not couples_axes(). The base returns true for any non-identity remap, including pure permutations that do not physically couple axes, so the question is "must every axis word be emitted", not a statement about kinematics. * Every per-site word-omission branch is preserved. The base deliberately emits X/Y only, or Z only, or drops Z when its quantised value is unchanged. The strategy changes which transform applies, never whether words are omitted. * set_kinematics() replays the configured remap and build volume onto a newly installed strategy, because BeltGCode::init_belt_writer runs before GCode.cpp calls set_axis_remap/set_build_volume_max. * uses_pointwise_travel_speed() preserves a pre-existing divergence rather than introducing one: the base travel_to_xyz emits the raw configured travel speed in its final branch, ignoring the first-layer value computed at the top, whereas the belt path used the first-layer-aware value throughout. Both are kept. Unifying them changes feedrates and belongs in its own change. * The [BELT-DEBUG] block is deleted; it rate-limited itself with a function-local static thread_local in the hot emission path, and this is the commit that would otherwise have moved it into shared code. This commit is intended to preserve existing export output. That is reviewed by construction -- each emission site keeps its own omission branch and each policy divergence is preserved -- and is NOT verified against a G-code diff corpus. Building that corpus is the outstanding work here. Two API-equivalence exceptions, neither reachable by any caller today: * Belt kinematics with no plane pointer installed, m_is_first_layer true, initial and normal travel speeds differing, travel_to_xyz() reaching its final branch: the old belt writer selected the initial-layer speed, the new writer selects the normal travel speed. The pending-lift and XY-only branches keep their previous selection. * Belt kinematics installed without set_force_normal_lift(true) and a non-normal lift requested: the old belt writer forced a normal lift, the new writer can take the slope branch. The PA-pattern generator reaches the writer through explicit travel_to_z() / travel_to_xy(), not travel_to_xyz() or the lazy/eager lift paths, and normal belt export installs both the plane and the forced-normal-lift policy, so neither exception changes output produced today. They are recorded because a future caller could reach them. tests/fff_print/test_gcodewriter.cpp was also not compiling before this branch: it called writer.to_machine_coords(), a method that existed only on BeltGCodeWriter. It never surfaced because the build targets OrcaSlicer, not all, and BUILD_TESTS defaults to OFF, so that translation unit was outside every compile path. Fixed here; the existing 30-degree coordinate assertions are kept verbatim as the best available regression net. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/BeltGCode.cpp | 12 +- src/libslic3r/BeltGCode.hpp | 2 +- src/libslic3r/BeltGCodeWriter.cpp | 277 ------------------ src/libslic3r/BeltGCodeWriter.hpp | 64 ---- src/libslic3r/CMakeLists.txt | 6 +- src/libslic3r/FirstLayerPlane.cpp | 2 +- src/libslic3r/GCode.cpp | 19 +- src/libslic3r/GCode.hpp | 2 +- src/libslic3r/GCode/BeltBackTransform.hpp | 2 +- src/libslic3r/GCode/BeltKinematics.cpp | 40 +++ src/libslic3r/GCode/BeltKinematics.hpp | 66 +++++ src/libslic3r/GCode/GCodeProcessor.cpp | 4 +- src/libslic3r/GCode/MachineFrameTransform.hpp | 2 +- src/libslic3r/GCode/MachineKinematics.cpp | 47 +++ src/libslic3r/GCode/MachineKinematics.hpp | 95 ++++++ src/libslic3r/GCodeWriter.cpp | 68 +++-- src/libslic3r/GCodeWriter.hpp | 60 +++- src/libslic3r/calib.cpp | 12 +- src/libslic3r/calib.hpp | 2 +- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/calib_dlg.cpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 16 +- 23 files changed, 398 insertions(+), 406 deletions(-) delete mode 100644 src/libslic3r/BeltGCodeWriter.cpp delete mode 100644 src/libslic3r/BeltGCodeWriter.hpp create mode 100644 src/libslic3r/GCode/BeltKinematics.cpp create mode 100644 src/libslic3r/GCode/BeltKinematics.hpp create mode 100644 src/libslic3r/GCode/MachineKinematics.cpp create mode 100644 src/libslic3r/GCode/MachineKinematics.hpp diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 3ba2f79e55..f26c7f87ff 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -1,5 +1,6 @@ #include "BeltGCode.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "BeltTransform.hpp" #include "Print.hpp" @@ -10,11 +11,12 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) if (!print.config().belt_printer.value) return; - auto belt_writer = std::make_unique(); + auto belt_writer = std::make_unique(); belt_writer->set_is_bbl_machine(is_bbl_printers); - // Axis remap and build volume max are set by base GCode after init_belt_writer returns. - belt_writer->set_belt_back_transform(print.config()); - belt_writer->set_machine_frame_transform(print.config()); + // Axis remap and build volume max are set by base GCode after init_belt_writer + // returns; set_kinematics() replays them, so install order does not matter. + install_belt_kinematics(*belt_writer, print.config()); + belt_writer->set_force_normal_lift(true); m_writer = std::move(belt_writer); } diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index 2a3718b49f..f52c62c28f 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -7,7 +7,7 @@ namespace Slic3r { // Belt-printer-specific GCode export. // // Inherits from GCode and overrides virtual hooks to: -// - Create a BeltGCodeWriter instead of a plain GCodeWriter +// - Install a BeltKinematics on the GCodeWriter // - Write belt configuration to the G-code header // - Adjust the origin for global pre-slice transforms when switching instances // - Disable arc fitting (G2/G3 not supported on belt printers) diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp deleted file mode 100644 index 4824d76f69..0000000000 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ /dev/null @@ -1,277 +0,0 @@ -#include "BeltGCodeWriter.hpp" -#include "FirstLayerPlane.hpp" -#include "Geometry.hpp" -#include - -namespace Slic3r { - -namespace { - -// Decide whether a particular destination point gets first-layer treatment. -// When the plane evaluator is active, distance from the plane wins; otherwise -// fall back to the layer-coarse m_is_first_layer flag set by the caller. -inline bool belt_point_on_first_layer( - const FirstLayerPlane *plane, - double first_layer_thickness_mm, - bool layer_first_flag, - const Vec3d &point_slicing_mm) -{ - if (plane && plane->is_active()) - return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm); - return layer_first_flag; -} - -} // namespace - -// ---- Belt configuration --------------------------------------------------- - -void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) -{ - m_belt_back_transform.init_from_config(config); -} - -void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) -{ - m_machine_frame_transform.init_from_config(config); -} - -Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const -{ - // Step 1+2: To Cartesian (back_transform + axis_remap). - // 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 - // as a global linear transform on the placed coords. - Vec3d final = m_machine_frame_transform.apply(result); - - // [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z - // crosses an integer mm boundary) to keep the log volume manageable while - // still capturing one sample per ~5 layers. Shows the full pipeline so - // Case A vs Case B can be compared step-by-step. - static thread_local int s_last_logged_z = std::numeric_limits::min(); - int z_bucket = static_cast(std::floor(pos.z() * 5.0)); // every 0.2mm - if (z_bucket != s_last_logged_z) { - s_last_logged_z = z_bucket; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords" - << " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")" - << " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")" - << " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")" - << " final=(" << final.x() << "," << final.y() << "," << final.z() << ")" - << " mft_active=" << m_machine_frame_transform.is_active() - << " back_active=" << m_belt_back_transform.is_active(); - } - return final; -} - -// ---- Overridden movement methods ------------------------------------------ - -std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment) -{ - m_pos(0) = point(0); - m_pos(1) = point(1); - - this->set_current_position_clear(true); - Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; - - // Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - - GCodeG1Formatter w; - w.emit_xyz(machine); - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(point.x(), point.y(), m_pos.z())); - auto speed = first_layer_for_point - ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - w.emit_f(speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) -{ - // Belt printer: force NormalLift since SpiralLift and SlopeLift compute - // slope angles that don't account for the YZ coordinate rotation. - return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase); -} - -std::string BeltGCodeWriter::eager_lift(const LiftType type) -{ - // Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation). - return GCodeWriter::eager_lift(LiftType::NormalLift); -} - -std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) -{ - m_pos(2) = z; - - double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); - if (speed == 0.) { - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(m_pos.x(), m_pos.y(), z)); - speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - } - - // Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords. - Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); - - GCodeG1Formatter w; - w.emit_xyz(machine); - w.emit_f(speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion) -{ - m_pos(0) = point(0); - m_pos(1) = point(1); - if (std::abs(dE) <= std::numeric_limits::epsilon()) - force_no_extrusion = true; - - if (!force_no_extrusion) - filament()->extrude(dE); - - Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; - - // Belt printer: transform and emit XYZ (Y and Z are coupled) - Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); - - GCodeG1Formatter w; - w.emit_xyz(machine); - if (!force_no_extrusion) - w.emit_e(filament()->E()); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion) -{ - m_pos = point; - m_lifted = 0; - if (!force_no_extrusion) - filament()->extrude(dE); - - Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; - point_on_plate = to_machine_coords(point_on_plate); - - GCodeG1Formatter w; - w.emit_xyz(point_on_plate); - if (!force_no_extrusion) - w.emit_e(filament()->E()); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - return w.string(); -} - -std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z) -{ - // Belt-specific override of travel_to_xyz. - // Key differences from base: - // 1. All coordinates go through to_machine_coords() - // 2. Always emit full XYZ (can't split XY and Z due to coupling) - // 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides) - - Vec3d dest_point = point; - const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); - auto travel_speed = - first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); - - // Handle pending z_hop - if (std::abs(m_to_lift) > EPSILON) { - assert(std::abs(m_lifted) < EPSILON); - if ((!this->is_current_position_clear() || m_pos != dest_point) && - m_to_lift + m_pos(2) > point(2)) { - m_lifted = m_to_lift + m_pos(2) - point(2); - dest_point(2) = m_to_lift + m_pos(2); - } - m_to_lift = 0.; - - std::string slop_move; - Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) }; - Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - Vec3d delta = target - source; - Vec2d delta_no_z = { delta(0), delta(1) }; - - if (delta(2) > 0 && delta_no_z.norm() != 0.0f) { - // Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift), - // but handle NormalLift and fallthrough. - if (m_to_lift_type == LiftType::SlopeLift && - this->is_current_position_clear() && - atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) { - Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope()); - Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source; - slope_top_point = to_machine_coords(slope_top_point); - GCodeG1Formatter w0; - w0.emit_xyz(slope_top_point); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - slop_move = w0.string(); - } - else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) { - // Only lift-in-place when the current position is known. On a normal - // printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled - // to Y/X, so _travel_to_z re-emits the current m_pos through the belt - // shear. At print start (and after custom gcode) m_pos.xy is still the - // uninitialised origin (0,0), which shears into a bogus machine point - // (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate - // lift here is safe: there is nothing to lift over yet, and the - // xy_z_move below travels straight to the destination with full XYZ, - // establishing the correct position. This mirrors the SlopeLift branch - // above, which already guards on is_current_position_clear(). - slop_move = _travel_to_z(target.z(), "normal lift Z"); - } - } - - std::string xy_z_move; - { - Vec3d emit_target = to_machine_coords(target); - GCodeG1Formatter w0; - // Belt mode: always emit full XYZ since Y and Z are coupled - w0.emit_xyz(emit_target); - w0.emit_f(travel_speed * 60.0); - w0.emit_comment(GCodeWriter::full_gcode_comment, comment); - xy_z_move = w0.string(); - } - m_pos = dest_point; - this->set_current_position_clear(true); - return slop_move + xy_z_move; - } - else if (!force_z && !this->will_move_z(point(2))) { - double nominal_z = m_pos(2) - m_lifted; - m_lifted -= (point(2) - nominal_z); - if (std::abs(m_lifted) < EPSILON) - m_lifted = 0.; - this->set_current_position_clear(true); - return this->travel_to_xy(to_2d(point)); - } - else { - m_lifted = 0; - } - - Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) }; - point_on_plate = to_machine_coords(point_on_plate); - - // Belt mode: always emit full XYZ - GCodeG1Formatter w; - w.emit_xyz(point_on_plate); - // Use the first-layer-aware travel_speed computed at the top of this function, - // not the raw config travel_speed, so initial-layer travels are correctly slowed. - w.emit_f(travel_speed * 60.0); - w.emit_comment(GCodeWriter::full_gcode_comment, comment); - - m_pos = dest_point; - this->set_current_position_clear(true); - return w.string(); -} - -} // namespace Slic3r diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp deleted file mode 100644 index ff7428d062..0000000000 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include "GCodeWriter.hpp" -#include "GCode/BeltBackTransform.hpp" -#include "GCode/MachineFrameTransform.hpp" - -namespace Slic3r { - -class FirstLayerPlane; - -// Belt-printer-specific GCode writer. -// -// Inherits from GCodeWriter and overrides movement methods to apply -// coordinate transformation (back-transform, axis remap, machine-frame -// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt). -class BeltGCodeWriter : public GCodeWriter -{ -public: - BeltGCodeWriter() : GCodeWriter() {} - - // Belt configuration (axis remap is inherited from GCodeWriter) - void set_belt_back_transform(const PrintConfig &config); - 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 - // of the plane (regardless of slicing layer index). - void set_first_layer_plane(const FirstLayerPlane *plane, - double first_layer_height_mm) { - m_first_layer_plane = plane; - m_first_layer_thickness_mm = first_layer_height_mm; - } - - // Overridden movement methods - std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override; - std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override; - std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; - std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override; - std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override; - std::string eager_lift(const LiftType type) override; - -protected: - std::string _travel_to_z(double z, const std::string &comment) override; - -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.; -}; - -} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index be34f6ba9e..1787c1adef 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -84,8 +84,6 @@ set(lisbslic3r_sources BeltBrim.hpp BeltGCode.cpp BeltGCode.hpp - BeltGCodeWriter.cpp - BeltGCodeWriter.hpp BeltPurge.cpp BeltSliceStrategy.cpp BeltSliceStrategy.hpp @@ -243,6 +241,10 @@ set(lisbslic3r_sources GCode/BeltBackTransform.hpp GCode/MachineFrameTransform.cpp GCode/MachineFrameTransform.hpp + GCode/BeltKinematics.cpp + GCode/BeltKinematics.hpp + GCode/MachineKinematics.cpp + GCode/MachineKinematics.hpp GCode/ConflictChecker.cpp GCode/ConflictChecker.hpp GCode/CoolingBuffer.cpp diff --git a/src/libslic3r/FirstLayerPlane.cpp b/src/libslic3r/FirstLayerPlane.cpp index 6a4feba886..f668ea774e 100644 --- a/src/libslic3r/FirstLayerPlane.cpp +++ b/src/libslic3r/FirstLayerPlane.cpp @@ -56,7 +56,7 @@ MachineZAffine compute_machine_z_affine(const PrintConfig &config) out.constant = trans; if (config.gcode_back_transform.value && config.belt_printer.value) { - // BeltGCodeWriter applies F^-1 before R when back-transform is on. + // BeltKinematics applies F^-1 before R when back-transform is on. // So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans // = (r_row^T · F^-1) · slicing + trans // We need to compose r_row with F^-1 from the LEFT (treating r_row as diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f95f59dceb..32976fad76 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2986,8 +2986,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // circuit to the legacy Layer::id() == 0 path so g-code stays bit- // identical to the pre-feature behavior. m_first_layer_plane = std::make_unique(print.config()); - if (auto *belt_writer = dynamic_cast(m_writer.get())) { - belt_writer->set_first_layer_plane( + // Belt writers only: the plane also switches travel-speed selection to be + // per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not + // change for non-belt printers. + if (print.config().belt_printer.value) { + m_writer->set_first_layer_plane( m_first_layer_plane.get(), print.config().initial_layer_print_height.value); } @@ -3888,12 +3891,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // 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); + const bool belt_world_coords = print.config().belt_printer.value; + if (belt_world_coords) + install_belt_kinematics(*m_writer, print.config(), /*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); + if (belt_world_coords) + install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/false); file.write(gcode); } else { @@ -8400,7 +8403,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| // is always 1 and this is currently a no-op; it is retained as a guard in // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame - // shear/scale acts on the g-code in BeltGCodeWriter, not here.) + // shear/scale acts on the g-code in BeltKinematics, not here.) if (m_config.belt_printer.value) { double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); if (det > EPSILON) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 17a7ce4608..56496f1b4e 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -4,7 +4,7 @@ #include "libslic3r.h" #include "ExPolygon.hpp" #include "GCodeWriter.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "FirstLayerPlane.hpp" #include "Layer.hpp" #include "Point.hpp" diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index b98770c688..878ae12b5a 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -13,7 +13,7 @@ namespace Slic3r { // machine's real coordinate space. // // Initialized once from PrintConfig, then applied per-point in -// GCodeWriter::to_machine_coords() before axis remapping. +// BeltKinematics::to_machine() before axis remapping. // // Active when gcode_back_transform is true AND at least one of: // - a shear axis has global mode enabled, or diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp new file mode 100644 index 0000000000..155e18d32d --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -0,0 +1,40 @@ +#include "BeltKinematics.hpp" +#include "../BeltTransform.hpp" +#include "../PrintConfig.hpp" +#include "../GCodeWriter.hpp" + +namespace Slic3r { + +BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates) + : m_world_coordinates(world_coordinates) +{ + m_back_active = m_back_transform.init_from_config(config); + m_machine_frame.init_from_config(config); + if (m_back_active) + // BeltBackTransform stores the inverse of this; keep the forward so + // to_logical() can reverse the whole chain. + m_back_forward = BeltTransformPipeline::build_forward_transform(config); +} + +Vec3d BeltKinematics::to_machine(const Vec3d &p) const +{ + const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p); + const Vec3d after_remap = this->apply_axis_remap(after_back); + return m_machine_frame.apply(after_remap); +} + +Vec3d BeltKinematics::to_logical(const Vec3d &machine) const +{ + const Vec3d before_frame = m_machine_frame.apply_inverse(machine); + const Vec3d before_remap = this->apply_axis_remap_inverse(before_frame); + if (m_world_coordinates || ! m_back_active) + return before_remap; + return m_back_forward * before_remap; +} + +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates) +{ + writer.set_kinematics(std::make_unique(config, world_coordinates)); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp new file mode 100644 index 0000000000..a57f18dcdd --- /dev/null +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -0,0 +1,66 @@ +#ifndef slic3r_BeltKinematics_hpp_ +#define slic3r_BeltKinematics_hpp_ + +#include "MachineKinematics.hpp" +#include "BeltBackTransform.hpp" +#include "MachineFrameTransform.hpp" + +namespace Slic3r { + +class PrintConfig; +class GCodeWriter; + +// Belt-printer machine frame. +// +// Forward order, as applied per emitted point: +// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) ) +// +// i.e. the slicer->world back-transform runs FIRST and the machine-frame +// shear/scale LAST, so the latter acts as a global linear transform on the +// already-placed coordinates. +// +// world_coordinates mode (the PA line / PA pattern calibration generators) +// treats the incoming point as already relative to the belt surface -- X across, +// Y along the belt, Z above it -- and therefore skips the back-transform while +// keeping the remap and the machine frame. It is a different coordinate map, not +// a writer mode, which is why it is fixed at construction. +class BeltKinematics : public CartesianKinematics +{ +public: + explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false); + + Vec3d to_machine(const Vec3d &p) const override; + Vec3d to_logical(const Vec3d &machine) const override; + // Machine -> build-volume frame. Only the machine-frame shear/scale is undone, + // matching what GCodeProcessor's bounds validation wants. This is deliberately + // NOT to_logical(). + Vec3d to_build_volume(const Vec3d &machine) const override + { return m_machine_frame.apply_inverse(machine); } + + // A belt writer has always emitted full XYZ on every move, whether or not any + // individual stage reports itself active. Making this conditional would change + // emitted G-code for an identity-transform belt configuration. + bool must_emit_all_axes() const override { return true; } + bool suppress_lift_at_unknown_position() const override { return true; } + + bool world_coordinates() const { return m_world_coordinates; } + +private: + BeltBackTransform m_back_transform; + MachineFrameTransform m_machine_frame; + // Forward of what m_back_transform inverts, kept so to_logical() can undo it. + Transform3d m_back_forward { Transform3d::Identity() }; + bool m_back_active { false }; + bool m_world_coordinates { false }; +}; + +// Install a belt machine frame on any GCodeWriter. Any axis remap and build +// volume already configured on the writer are carried over, so this may be +// called before or after those setters. Re-calling it with a different +// world_coordinates value swaps the map (used around the PA line generator). +void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, + bool world_coordinates = false); + +} // namespace Slic3r + +#endif // slic3r_BeltKinematics_hpp_ diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ca41547ff3..2112cac8fd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2774,7 +2774,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int }; // Belt-printer post-gcode shear/scale/post_remap is applied as the final - // step of BeltGCodeWriter::to_machine_coords, so MoveVertex.position is + // step of BeltKinematics::to_machine, so MoveVertex.position is // in the printer's machine frame. Undo it here so XY area and Z height // checks operate in the build-volume frame that printable_area / // printable_height are defined in. For non-belt printers @@ -7111,7 +7111,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type, // During the start G-code "prepare" stage the toolhead Z is not yet a real // print height on a normal printer, so it is pinned to the first-layer height // to keep the preview tidy. Belt printers are the exception: there the Z is - // written explicitly by BeltGCodeWriter and the designed-view back-transform + // written explicitly by the belt kinematics and the designed-view back-transform // couples machine Z into the rendered model Y (the belt tilt mixes the height // and belt-feed axes). Overriding Z therefore back-transforms the last // prepare-stage move (the unretract before the first extrusion) to model diff --git a/src/libslic3r/GCode/MachineFrameTransform.hpp b/src/libslic3r/GCode/MachineFrameTransform.hpp index 83519bdbe4..cdae28ca1a 100644 --- a/src/libslic3r/GCode/MachineFrameTransform.hpp +++ b/src/libslic3r/GCode/MachineFrameTransform.hpp @@ -9,7 +9,7 @@ namespace Slic3r { // Post-stage machine-frame transform for belt printers. // -// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform and +// Applied in BeltKinematics::to_machine AFTER the back-transform and // the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code // coordinates into the printer's physical machine frame. // diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp new file mode 100644 index 0000000000..05f5375aef --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -0,0 +1,47 @@ +#include "MachineKinematics.hpp" + +namespace Slic3r { + +// Moved verbatim from GCodeWriter::apply_axis_remap(). +Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const +{ + if (!has_axis_remap()) + return pos; + auto remap = [this, &pos](int r) -> double { + int axis = r % 3; + if (r < 3) return pos[axis]; + if (r < 6) return -pos[axis]; + return m_build_vol_max[axis] - pos[axis]; + }; + return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; +} + +// Inverse of the above. Output axis i is fed by source axis (r_i % 3); walking +// the three outputs therefore fills every source component exactly once, so long +// as the remap is a permutation (which set_axis_remap callers guarantee). +Vec3d CartesianKinematics::apply_axis_remap_inverse(const Vec3d &machine) const +{ + if (!has_axis_remap()) + return machine; + Vec3d out = Vec3d::Zero(); + const int r[3] = { m_remap_x, m_remap_y, m_remap_z }; + for (int i = 0; i < 3; ++i) { + const int axis = r[i] % 3; + if (r[i] < 3) out[axis] = machine[i]; + else if (r[i] < 6) out[axis] = -machine[i]; + else out[axis] = m_build_vol_max[axis] - machine[i]; + } + return out; +} + +Vec3d CartesianKinematics::to_machine(const Vec3d &p) const +{ + return this->apply_axis_remap(p); +} + +Vec3d CartesianKinematics::to_logical(const Vec3d &machine) const +{ + return this->apply_axis_remap_inverse(machine); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp new file mode 100644 index 0000000000..c3e381d851 --- /dev/null +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -0,0 +1,95 @@ +#ifndef slic3r_MachineKinematics_hpp_ +#define slic3r_MachineKinematics_hpp_ + +#include "../Point.hpp" + +namespace Slic3r { + +// The frame contract for emitted movement. +// +// GCodeWriter produces points in the *logical placed* frame: plate offsets have +// already been subtracted, but no machine-specific mapping has been applied. +// A MachineKinematics turns that into the coordinates actually written to +// G-code, and answers the two structural questions the writer needs in order to +// decide which axis words it may omit. +// +// This is a seam for writer-generated movement only. Start/end/custom G-code, +// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass +// through it; see doc in 09-orca-machinekinematics-split-plan.md section 6. +class MachineKinematics +{ +public: + virtual ~MachineKinematics() = default; + + // Logical placed point -> emitted machine point. + virtual Vec3d to_machine(const Vec3d &p) const = 0; + + // Inverse of to_machine(), back to the logical placed frame. Intended for + // consumers that must reconstruct model coordinates from emitted G-code + // (the G-code viewer's upright preview). + virtual Vec3d to_logical(const Vec3d &machine) const = 0; + + // Machine point -> build-volume frame, for bounds validation only. This is + // deliberately NOT to_logical(): the build-volume check wants the physical + // frame the printable area is expressed in, not the model frame. Keeping + // them separate stops the two contracts from being confused. + virtual Vec3d to_build_volume(const Vec3d &machine) const = 0; + + // True when a move must emit X, Y and Z because omitting a word would be + // wrong under this mapping. Deliberately not called "couples_axes": a pure + // axis permutation forces full emission without physically coupling axes. + virtual bool must_emit_all_axes() const = 0; + + // True when a separate in-place lift must be suppressed while the current + // position is unknown, because _travel_to_z() re-emits the logical X/Y + // through this mapping and an uninitialised position would map to a bogus + // machine point. + virtual bool suppress_lift_at_unknown_position() const = 0; + + // Configuration. GCodeWriter forwards its setters here so that the state + // lives with the strategy and a strategy installed before the setters run + // still receives it. + virtual void set_axis_remap(int rx, int ry, int rz) = 0; + virtual void set_build_volume_max(const Vec3d &max) = 0; +}; + +// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim. +// +// The remap encodes, per output axis, which source axis feeds it and how: +// r < 3 : source axis r, unchanged +// r < 6 : source axis r-3, negated +// else : source axis r-6, reversed within the build volume +class CartesianKinematics : public MachineKinematics +{ +public: + Vec3d to_machine(const Vec3d &p) const override; + Vec3d to_logical(const Vec3d &machine) const override; + Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } + + bool must_emit_all_axes() const override { return this->has_axis_remap(); } + // The base writer has never suppressed the lift, not even under a remap that + // makes _travel_to_z re-emit X/Y. That is arguably a latent bug, but fixing + // it here would change emitted G-code, so today's behaviour is preserved and + // the divergence from BeltKinematics is deliberate. + bool suppress_lift_at_unknown_position() const override { return false; } + + void set_axis_remap(int rx, int ry, int rz) override + { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; } + void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; } + + bool has_axis_remap() const + { return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; } + +protected: + Vec3d apply_axis_remap(const Vec3d &pos) const; + Vec3d apply_axis_remap_inverse(const Vec3d &pos) const; + + int m_remap_x { 0 }; + int m_remap_y { 1 }; + int m_remap_z { 2 }; + Vec3d m_build_vol_max { Vec3d::Zero() }; +}; + +} // namespace Slic3r + +#endif // slic3r_MachineKinematics_hpp_ diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 67dd6cf138..be8f6607de 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,4 +1,5 @@ #include "GCodeWriter.hpp" +#include "FirstLayerPlane.hpp" #include "CustomGCode.hpp" #include "Geometry.hpp" #include "I18N.hpp" @@ -24,34 +25,46 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const +{ + if (m_first_layer_plane && m_first_layer_plane->is_active()) + return m_first_layer_plane->is_first_layer(point_logical, m_first_layer_thickness_mm); + return m_is_first_layer; +} + void GCodeWriter::set_axis_remap(int rx, int ry, int rz) { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; + m_kinematics->set_axis_remap(rx, ry, rz); } void GCodeWriter::set_build_volume_max(const Vec3d &max) { m_build_vol_max = max; + m_kinematics->set_build_volume_max(max); } +void GCodeWriter::set_kinematics(std::unique_ptr kinematics) +{ + assert(kinematics); + m_kinematics = std::move(kinematics); + // Replay whatever was configured on the previous strategy so callers may + // install the kinematics before or after set_axis_remap/set_build_volume_max. + m_kinematics->set_axis_remap(m_remap_x, m_remap_y, m_remap_z); + m_kinematics->set_build_volume_max(m_build_vol_max); +} + +// Kept as the writer-facing name for "this move must emit every axis word". bool GCodeWriter::has_axis_remap() const { - return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; + return m_kinematics->must_emit_all_axes(); } Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const { - if (!has_axis_remap()) - return pos; - auto remap = [this, &pos](int r) -> double { - int axis = r % 3; - if (r < 3) return pos[axis]; - if (r < 6) return -pos[axis]; - return m_build_vol_max[axis] - pos[axis]; - }; - return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) }; + return m_kinematics->to_machine(pos); } bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) @@ -795,7 +808,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com } else { w.emit_xy(point_on_plate); } - auto speed = m_is_first_layer + auto speed = this->point_on_first_layer(Vec3d(point.x(), point.y(), m_pos.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); w.emit_f(speed * 60.0); //BBS @@ -808,6 +821,8 @@ it will not perform subsequent lifts, even if Z was raised manually (i.e. with travel_to_z()) and thus _lifted was reduced. */ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) { + if (m_force_normal_lift) + lift_type = LiftType::NormalLift; // check whether the above/below conditions are met double target_lift = 0; { @@ -836,7 +851,7 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - const LiftType effective_type = type; + const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type; std::string lift_move; double target_lift = 0; { @@ -888,7 +903,12 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co // BBS Vec3d dest_point = point; auto travel_speed = - m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); + this->point_on_first_layer(point) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); + // See uses_pointwise_travel_speed(): the historical path deliberately emits the + // raw configured speed in the final branch below, ignoring travel_speed. + const double final_travel_speed = this->uses_pointwise_travel_speed() + ? travel_speed + : this->config.travel_speed.get_at(m_cached_extruder_idx); //BBS: a z_hop need to be handle when travel if (std::abs(m_to_lift) > EPSILON) { assert(std::abs(m_lifted) < EPSILON); @@ -946,7 +966,16 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co w0.emit_comment(GCodeWriter::full_gcode_comment, comment); slop_move = w0.string(); } - else if (m_to_lift_type == LiftType::NormalLift) { + else if (m_to_lift_type == LiftType::NormalLift && + (! m_kinematics->suppress_lift_at_unknown_position() || + this->is_current_position_clear())) { + // Only lift in place when the current position is known, for a mapping + // that makes _travel_to_z re-emit logical X/Y: at print start (and after + // custom gcode) m_pos.xy is still the uninitialised origin, which would + // map to a bogus machine point. The xy_z_move below then travels straight + // to the destination with full XYZ and establishes the correct position. + // Mappings that do not need this (the historical Cartesian behaviour) + // report false and keep lifting unconditionally. slop_move = _travel_to_z(target.z(), "normal lift Z"); } } @@ -1002,20 +1031,20 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co if (has_axis_remap()) { // Remap may couple XY with Z; emit full XYZ in machine coordinates. w.emit_xyz(apply_axis_remap(point_on_plate)); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string(); } else if (!this->is_current_position_clear()) { //force to move xy first then z after filament change w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y())); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string() + _travel_to_z(point_on_plate.z(), comment); } else { GCodeG1Formatter w; w.emit_xyz(point_on_plate); - w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0); + w.emit_f(final_travel_speed * 60.0); w.emit_comment(GCodeWriter::full_gcode_comment, comment); out_string = w.string(); } @@ -1050,8 +1079,9 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment) double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx); if (speed == 0.) { - speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) - : this->config.travel_speed.get_at(m_cached_extruder_idx); + speed = this->point_on_first_layer(Vec3d(m_pos.x(), m_pos.y(), z)) + ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) + : this->config.travel_speed.get_at(m_cached_extruder_idx); } GCodeG1Formatter w; diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index da2ce35cd6..f5e037d685 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -9,8 +9,12 @@ #include "Polygon.hpp" #include "PrintConfig.hpp" #include "GCode/CoolingBuffer.hpp" +#include "GCode/MachineKinematics.hpp" +#include namespace Slic3r { +class FirstLayerPlane; + class GCodeWriter { public: virtual ~GCodeWriter() = default; @@ -28,7 +32,8 @@ public: m_lifted(0), m_to_lift(0), m_to_lift_type(LiftType::NormalLift), - m_current_speed(3600), m_is_first_layer(true) + m_current_speed(3600), m_is_first_layer(true), + m_kinematics(std::make_unique()) {} Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } @@ -142,10 +147,28 @@ public: void set_build_volume_max(const Vec3d &max); bool has_axis_remap() const; + // Install the machine frame mapping. Any axis remap / build volume already + // configured is carried over, so install order does not matter. + void set_kinematics(std::unique_ptr kinematics); + const MachineKinematics& kinematics() const { return *m_kinematics; } + + // First-layer plane evaluator. When set to an active plane, travel speed + // selection consults the plane per destination point instead of the + // layer-coarse m_is_first_layer flag. Borrowed pointer; lifetime is owned + // by GCode, which constructs the plane after the writer exists -- so this is + // deliberately a setter and not a constructor argument. + void set_first_layer_plane(const FirstLayerPlane *plane, double first_layer_height_mm) + { m_first_layer_plane = plane; m_first_layer_thickness_mm = first_layer_height_mm; } + + // Force every lift to a plain vertical lift. Spiral and slope lifts compute + // their slope in the logical frame and do not account for a machine mapping + // that couples axes. + void set_force_normal_lift(bool force) { m_force_normal_lift = force; } + // Returns whether this flavor supports separate print and travel acceleration. static bool supports_separate_travel_acceleration(GCodeFlavor flavor); protected: - // Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter) + // Position/lift/offset state. Vec3d m_pos = Vec3d::Zero(); double m_x_offset{ 0 }; double m_y_offset{ 0 }; @@ -158,17 +181,44 @@ protected: virtual std::string _travel_to_z(double z, const std::string &comment); - // Axis remap state — accessible to subclasses. + // Whether a destination gets first-layer treatment. With an active plane + // evaluator, distance from the plane decides; otherwise the layer-coarse + // m_is_first_layer flag does. + bool point_on_first_layer(const Vec3d &point_logical) const; + + // True when travel speed is selected per destination point rather than per + // layer. Set for writers that install a first-layer plane. The historical + // path emits the raw configured travel speed in the final branch of + // travel_to_xyz(), ignoring the first-layer selection computed at the top of + // that function; a plane-driven writer uses the first-layer-aware value + // throughout. Both are preserved exactly -- unifying them would change + // emitted feedrates and belongs in its own commit. + bool uses_pointwise_travel_speed() const { return m_first_layer_plane != nullptr; } + + // Borrowed; null = inactive. + const FirstLayerPlane *m_first_layer_plane = nullptr; + double m_first_layer_thickness_mm = 0.; + bool m_force_normal_lift = false; + + // The machine frame mapping. Owns the axis-remap state that used to live + // here as m_remap_* / m_build_vol_max; the setters above forward to it. + // Never null: a CartesianKinematics at the identity remap reproduces the + // historical behaviour exactly. + std::unique_ptr m_kinematics; + + // Last configured remap / build volume, replayed onto a newly installed + // kinematics so set_kinematics() and the setters are order-independent. int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc. int m_remap_y = 1; int m_remap_z = 2; Vec3d m_build_vol_max = Vec3d::Zero(); - // Apply axis remap to a point. Returns pos unchanged if remap is identity. + // Apply the machine frame mapping to a point. Returns pos unchanged when the + // mapping is the identity. Vec3d apply_axis_remap(const Vec3d &pos) const; // Motion uses the global/base process variant until a filament becomes active. - // Protected so BeltGCodeWriter indexes the per-extruder speed options (travel_speed, + // Protected so subclasses index the per-extruder speed options (travel_speed, // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. size_t m_cached_extruder_idx; diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 620babba4d..cfb8112826 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,5 +1,5 @@ #include "calib.hpp" -#include "BeltGCodeWriter.hpp" +#include "GCode/BeltKinematics.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Model.hpp" @@ -855,10 +855,8 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod // 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); + auto belt_writer = std::make_shared(); + install_belt_kinematics(*belt_writer, print_config, /*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); @@ -869,7 +867,9 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod print_config.printable_height.value)); } m_writer = std::move(belt_writer); - } else if (dynamic_cast(m_writer.get()) != nullptr) { + } else if (m_writer && dynamic_cast(&m_writer->kinematics()) != nullptr) { + // Previously configured for a belt printer; drop back to a plain writer, + // exactly as the old dynamic_cast check did. m_writer = std::make_shared(); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index 109360f6ec..2478349bf0 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -377,7 +377,7 @@ private: const Calib_Params &m_params; - // Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates + // Polymorphic so belt printers get belt kinematics 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()}; diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 1b5c2df0b5..ca4867a68b 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1147,7 +1147,7 @@ std::vector GCodeViewer::get_plater_extruder() // Belt printers: compute the full machine->model back-transform from the print // config, so the "designed" (upright) G-code preview maps each toolpath vertex -// back to Cartesian space. The G-code forward pipeline is (BeltGCodeWriter:: +// back to Cartesian space. The G-code forward pipeline is (BeltKinematics:: // to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if // gcode_back_transform (write already un-rotated to Cartesian) else BeltForward( // model). So the inverse is: diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6a8b398871..1929b83de5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15760,7 +15760,7 @@ void Plater::_calib_apply_belt_mode() 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) + // (belt kinematics in world-coordinates mode draws them on the belt surface) // but are not validated yet — keep them gated to the PA Tower for now. { double angle_rad = 0.; diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index c0979ec56c..83e36dbd69 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -98,7 +98,7 @@ std::vector make_shaper_type_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 +// belt surface via BeltKinematics world-coordinates mode) but are not // validated yet — belt printers are restricted to the PA Tower for now. bool is_belt_printer_selected() { diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 22ca387bbb..522d945948 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -19,7 +19,7 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include #include -#include "libslic3r/BeltGCodeWriter.hpp" +#include "libslic3r/GCode/BeltKinematics.hpp" #include "libslic3r/BeltTransform.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/PrintConfig.hpp" @@ -39,9 +39,8 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo config.gcode_remap_y.value = RemapAxis::PosZ; config.gcode_remap_z.value = RemapAxis::PosY; - BeltGCodeWriter writer; - writer.set_belt_back_transform(config); - writer.set_machine_frame_transform(config); + GCodeWriter writer; + install_belt_kinematics(writer, config); writer.set_axis_remap(int(config.gcode_remap_x.value), int(config.gcode_remap_y.value), int(config.gcode_remap_z.value)); @@ -52,7 +51,7 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo // machine-frame shear/scale are applied. const Vec3d model(4., 10., 3.); Transform3d forward = BeltTransformPipeline::build_forward_transform(config); - const Vec3d machine = writer.to_machine_coords(forward * model); + const Vec3d machine = writer.kinematics().to_machine(forward * model); // The conventional X-tilt remap produces (x, z, y). At 30 degrees the // gantry coordinate is z/sin(30) and belt travel is y + z*cot(30). @@ -905,7 +904,7 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G // is_current_position_clear(), mirroring the SlopeLift branch. SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]") { - GIVEN("A fresh BeltGCodeWriter configured for an X-tilt 45 degree belt") { + GIVEN("A fresh belt-kinematics GCodeWriter configured for an X-tilt 45 degree belt") { // Machine-frame + slicer->world back-transform config (X tilt, 45 deg). PrintConfig belt_config; belt_config.belt_printer.value = true; @@ -917,9 +916,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin" belt_config.belt_frame_tilt_decouple.value = false; belt_config.belt_frame_tilt_angle.value = 45.0; - BeltGCodeWriter writer; - writer.set_machine_frame_transform(belt_config); - writer.set_belt_back_transform(belt_config); + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); std::vector extruder_ids { 0 }; writer.set_extruders(extruder_ids); From 4d2c3a0af4bcf9a3a813876b1280068a5f318af8 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:58:19 -0500 Subject: [PATCH 02/75] GCodeWriter: fix two machine-mapping bugs the extraction preserved Both change emitted G-code, which is why they were kept out of the extraction commit. Both are wrong only where the machine mapping is non-identity, which is the definition of each bug. 1. Suppress lifts commanded through an unknown position. _travel_to_z() emits full XYZ whenever the mapping must emit every axis, because the mapping can make machine Z depend on logical X/Y, and it builds that point from m_pos. At print start, and after any custom G-code that invalidates position, m_pos.xy is the uninitialised origin; mapping (0, 0, z) through a non-identity remap produces a real but wrong machine point -- for a reverse mapping, build_vol_max, i.e. the far corner of the bed. The subsequent full-XYZ move corrects the position, but the lift has already commanded a rapid across the whole bed at travel speed. Belt kinematics already guarded this; the Cartesian path did not. The guard is now applied at all three lift sites through must_skip_lift_now(), not just the one the extraction covered: travel_to_xyz()'s pending-lift branch, lazy_lift(spiral_vase=true), and eager_lift(). The latter two also needed the state fix -- both recorded m_lifted = target_lift regardless, so suppressing only the emission would leave a later unlift() descending from a height that was never commanded. 2. Never emit a G2/G3 arc a mapping cannot represent. extrude_arc_to_xy() emitted G2/G3 with logical X/Y and I/J and never consulted the mapping. There is no general fix by transforming the arc: a permutation moves it out of the XY plane that I/J describes, a negation reverses handedness, and the belt shear maps a circle to an ellipse that G2/G3 cannot express at all. So supports_arc_moves() gates generation through the existing GCode::should_disable_arc_fitting() hook, and BeltGCode's special-case override is deleted -- belt now gets the same behaviour from the general rule instead of its own exception. supports_arc_moves() is m_remap_x == 0 && m_remap_y == 1, not !has_axis_remap(): an arc emits only X/Y/I/J, so a mapping that merely negates or reverses Z leaves every emitted word untouched and keeps its arcs. The fallback for an unrepresentable arc tessellates it into linear segments at a 0.005mm chord tolerance rather than substituting a single chord, and splits dE proportionally across the segments. The capability check is hoisted above every extrusion mutation: an earlier form ran it after filament()->extrude(dE) and so extruded 2*dE on the fallback path. Known limits of that fallback, since it is worth stating rather than discovering: emitted relative E is conserved only to per-segment rounding (a radius-5 semicircle with dE=1.5 emits 1.50012 across 36 segments); the 0.005mm bound is a logical-frame bound, about 0.00855mm in machine space under a 45-degree belt shear; unequal endpoint radii and non-finite inputs are unchecked. Ordinary export takes the original polyline when the mapping rejects arcs, so this path is a fallback rather than the normal route. Known gap, not claimed fixed: classic wipe towers have their own enable_arc_fitting and their own G2/G3 emitter in GCode/WipeTower.cpp, which should_disable_arc_fitting() does not govern. Belt printers are barred from classic wipe towers; a remapped Cartesian printer is not. Tests in tests/fff_print/test_gcodewriter.cpp: reverse-X remap with unknown and with known position plus an identity control; eager_lift emitting nothing and recording nothing; the arc-capability matrix including the Z-only cases; and the tessellated fallback. E accounting is asserted through used_filament() rather than E(), which resets per line in relative-E mode. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/BeltGCode.hpp | 4 +- src/libslic3r/GCode.hpp | 6 +- src/libslic3r/GCode/BeltKinematics.hpp | 3 + src/libslic3r/GCode/MachineKinematics.hpp | 29 ++- src/libslic3r/GCodeWriter.cpp | 75 +++++- src/libslic3r/GCodeWriter.hpp | 8 + tests/fff_print/test_gcodewriter.cpp | 295 ++++++++++++++++++++++ 7 files changed, 404 insertions(+), 16 deletions(-) diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index f52c62c28f..9598e9ebd5 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -10,14 +10,14 @@ namespace Slic3r { // - Install a BeltKinematics on the GCodeWriter // - Write belt configuration to the G-code header // - Adjust the origin for global pre-slice transforms when switching instances -// - Disable arc fitting (G2/G3 not supported on belt printers) +// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(), +// which the base GCode::should_disable_arc_fitting() consults -- no override needed.) class BeltGCode : public GCode { protected: void init_belt_writer(Print &print, bool is_bbl_printers) override; void write_belt_header(GCodeOutputStream &file, const Print &print) override; void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; - bool should_disable_arc_fitting() const override { return true; } }; } // namespace Slic3r diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 56496f1b4e..26b8bc78a5 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -379,7 +379,11 @@ protected: virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} - virtual bool should_disable_arc_fitting() const { return false; } + // Arc fitting is suppressed whenever the writer's machine mapping cannot + // represent a G2/G3 arc. Belt printers get this through BeltKinematics + // rather than through an override of their own. + virtual bool should_disable_arc_fitting() const + { return ! m_writer->kinematics().supports_arc_moves(); } void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb); diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp index a57f18dcdd..f145ecf3e9 100644 --- a/src/libslic3r/GCode/BeltKinematics.hpp +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -42,6 +42,9 @@ public: // emitted G-code for an identity-transform belt configuration. bool must_emit_all_axes() const override { return true; } bool suppress_lift_at_unknown_position() const override { return true; } + // The machine frame shears and scales, so a circle is an ellipse in machine + // coordinates and G2/G3 cannot describe it. + bool supports_arc_moves() const override { return false; } bool world_coordinates() const { return m_world_coordinates; } diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp index c3e381d851..e046dd9476 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -40,12 +40,21 @@ public: // axis permutation forces full emission without physically coupling axes. virtual bool must_emit_all_axes() const = 0; - // True when a separate in-place lift must be suppressed while the current - // position is unknown, because _travel_to_z() re-emits the logical X/Y - // through this mapping and an uninitialised position would map to a bogus - // machine point. + // True when a lift must be suppressed while the current position is unknown, + // because _travel_to_z() re-emits the logical X/Y through this mapping and an + // uninitialised position would map to a bogus machine point -- for a reverse + // mapping, the far corner of the bed. virtual bool suppress_lift_at_unknown_position() const = 0; + // True when a G2/G3 arc in the logical XY plane is still the same arc in the + // machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower + // question than must_emit_all_axes(): whether logical X and Y reach the + // machine unchanged. A mapping that only negates or reverses Z keeps its + // arcs; one that permutes X or Y moves the arc out of the plane that I/J + // describes, and a shear turns the circle into an ellipse G2/G3 cannot + // express at all. + virtual bool supports_arc_moves() const = 0; + // Configuration. GCodeWriter forwards its setters here so that the state // lives with the strategy and a strategy installed before the setters run // still receives it. @@ -67,11 +76,13 @@ public: Vec3d to_build_volume(const Vec3d &machine) const override { return machine; } bool must_emit_all_axes() const override { return this->has_axis_remap(); } - // The base writer has never suppressed the lift, not even under a remap that - // makes _travel_to_z re-emit X/Y. That is arguably a latent bug, but fixing - // it here would change emitted G-code, so today's behaviour is preserved and - // the divergence from BeltKinematics is deliberate. - bool suppress_lift_at_unknown_position() const override { return false; } + bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); } + + // X and Y must reach the machine untouched. Because the remap is a + // permutation, pinning those two also pins Z to Z, so a mapping that only + // negates or reverses Z still supports arcs -- every word a G2/G3 emits is + // unchanged by it. + bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; } void set_axis_remap(int rx, int ry, int rz) override { m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; } diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index be8f6607de..72c720cac6 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -25,6 +25,15 @@ namespace Slic3r { bool GCodeWriter::full_gcode_comment = true; +// A lift emitted through _travel_to_z() re-emits the stored logical X/Y under a +// mapping that must emit every axis. While the position is unknown that X/Y is +// the uninitialised origin, which maps to a real but wrong machine point, so the +// lift has to be skipped rather than commanded. +bool GCodeWriter::must_skip_lift_now() const +{ + return m_kinematics->suppress_lift_at_unknown_position() && ! this->is_current_position_clear(); +} + bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const { if (m_first_layer_plane && m_first_layer_plane->is_active()) @@ -837,6 +846,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) { if (spiral_vase) { + if (this->must_skip_lift_now()) + // Record no lift, so a later unlift() does not descend from a + // height that was never commanded. + return ""; m_lifted = target_lift; return this->_travel_to_z(m_pos(2) + target_lift, "lift Z"); } @@ -882,7 +895,12 @@ std::string GCodeWriter::eager_lift(const LiftType type) { } //BBS: if position is unknown use normal lift else if (target_lift > 0) { - lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z"); + if (this->must_skip_lift_now()) + // Skipped, not deferred: leave m_lifted at zero below so unlift() + // does not descend from a height that was never commanded. + target_lift = 0.; + else + lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z"); } m_lifted = target_lift; m_to_lift = 0; @@ -966,9 +984,7 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co w0.emit_comment(GCodeWriter::full_gcode_comment, comment); slop_move = w0.string(); } - else if (m_to_lift_type == LiftType::NormalLift && - (! m_kinematics->suppress_lift_at_unknown_position() || - this->is_current_position_clear())) { + else if (m_to_lift_type == LiftType::NormalLift && ! this->must_skip_lift_now()) { // Only lift in place when the current position is known, for a mapping // that makes _travel_to_z re-emit logical X/Y: at print start (and after // custom gcode) m_pos.xy is still the uninitialised origin, which would @@ -1220,11 +1236,62 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std: return w.string(); } +// Approximate an arc with linear extrusions, for machine mappings that cannot +// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre +// relative to the CURRENT position, which is why this must run before m_pos is +// updated. +std::string GCodeWriter::extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, + double dE, const bool is_ccw, + const std::string &comment, bool force_no_extrusion) +{ + const Vec2d start = Vec2d(m_pos.x(), m_pos.y()); + const Vec2d centre = start + center_offset; + const double r = (start - centre).norm(); + if (r < EPSILON) + // Degenerate: no arc to speak of, so a single move is exact. + return this->extrude_to_xy(point, dE, comment, force_no_extrusion); + + double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x()); + double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x()); + double sweep = a1 - a0; + if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; } + else { while (sweep >= 0.) sweep -= 2. * PI; } + + // Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol. + const double tol = 0.005; // mm + const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r); + const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON)))); + + std::string out; + for (int i = 1; i <= n; ++ i) { + const double a = a0 + sweep * (double(i) / double(n)); + const Vec2d p = (i == n) ? point + : Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a)); + out += this->extrude_to_xy(p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion); + } + return out; +} + //BBS: generate G2 or G3 extrude which moves by arc //point is end point which means X and Y axis //center_offset is I and J axis std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion) { + // Arcs emit only X/Y/I/J, so a mapping that moves logical X or Y cannot be + // expressed as a G2/G3. GCode::should_disable_arc_fitting() normally stops + // arcs being generated at all for such a mapping, but this is public API, so + // define the behaviour rather than asserting. + // + // This check MUST precede every state mutation below: falling through to + // extrude_to_xy() after filament()->extrude(dE) would advance E twice. + // + // A single chord is not a safe substitute either -- a semicircle would become + // its diameter and a full circle a stationary blob -- so approximate the arc + // with linear segments bounded by a chord tolerance, splitting dE between + // them in proportion to arc length. + if (! m_kinematics->supports_arc_moves()) + return this->extrude_arc_as_polyline(point, center_offset, dE, is_ccw, comment, force_no_extrusion); + m_pos(0) = point(0); m_pos(1) = point(1); if (!force_no_extrusion) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index f5e037d685..a382f4abae 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -90,6 +90,10 @@ public: virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); //BBS: generate G2 or G3 extrude which moves by arc std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); + // Linear approximation of an arc, used when the machine mapping cannot + // express a G2/G3. Must be called before m_pos is updated: center_offset is + // relative to the current position. + std::string extrude_arc_as_polyline(const Vec2d &point, const Vec2d ¢er_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false); virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false); std::string retract(bool before_wipe = false, double retract_length = 0); std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0); @@ -186,6 +190,10 @@ protected: // m_is_first_layer flag does. bool point_on_first_layer(const Vec3d &point_logical) const; + // True when a lift must be skipped because this mapping would emit the + // stored logical X/Y and that position is not yet known. + bool must_skip_lift_now() const; + // True when travel speed is selected per destination point rather than per // layer. Set for writers that install a first-layer plane. The historical // path emits the raw configured travel speed in the final branch of diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 522d945948..18a720aaff 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -1032,3 +1032,298 @@ SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][bel } } } + +// --------------------------------------------------------------------------- +// Regression tests for the two latent bugs the MachineKinematics refactor +// preserved deliberately (see 09b-latent-bug-fix-plan.md). +// --------------------------------------------------------------------------- + +// Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every +// axis, and it builds that point from m_pos. While the position is unknown, +// m_pos.xy is the uninitialised origin, which a reverse remap maps to the far +// corner of the bed. Belt kinematics guarded this; a Cartesian writer with an +// axis remap did not, and would command a rapid across the whole bed. +static void configure_lift_writer(GCodeWriter &writer) +{ + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.config.travel_speed.values = { 100.0 }; + writer.config.travel_speed_z.values = { 100.0 }; + writer.config.z_hop.values = { 0.4 }; + writer.config.retract_lift_above.values = { 0.0 }; + writer.config.retract_lift_below.values = { 0.0 }; +} + +// Largest X word in a chunk of emitted G-code, or lowest() if none. +static double max_emitted_x(const std::string &gcode) +{ + double max_x = std::numeric_limits::lowest(); + GCodeReader reader; + reader.parse_buffer(gcode, [&max_x](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1") && line.has(X)) + max_x = std::max(max_x, double(line.x())); + }); + return max_x; +} + +static size_t count_g1(const std::string &gcode) +{ + size_t n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&n](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("G1")) ++n; + }); + return n; +} + +SCENARIO("Axis remap: no lift is commanded through the uninitialised origin", "[GCodeWriter][remap]") +{ + // Reverse X: machine X = build_vol_max.x - logical X, so the uninitialised + // origin maps to the far edge of the bed and is unmistakable in the output. + const double bed_x = 250.0; + + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("nothing is commanded at the image of the origin") { + // The destination maps to machine X = 250 - 10 = 240; the bogus + // origin lift would have mapped to machine X = 250. + REQUIRE(max_emitted_x(gcode) < bed_x - 1.0); + } + THEN("only the destination move is emitted") { + REQUIRE(count_g1(gcode) == 1); + } + } + } + + GIVEN("the same writer once its position is known") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0)); + writer.travel_to_xyz(Vec3d(20.0, 20.0, 5.0)); + REQUIRE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel again") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(30.0, 30.0, 5.0)); + + THEN("the separate lift move is still emitted") { + // Suppression must be pinned to the unknown position, not to the + // presence of a remap. + REQUIRE(count_g1(gcode) == 2); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + REQUIRE_FALSE(writer.kinematics().must_emit_all_axes()); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("a z-hop is pending and we travel to the first point") { + writer.lazy_lift(LiftType::NormalLift); + const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0)); + + THEN("behaviour is unchanged: the lift is still emitted") { + // Three moves, not two: with no remap and an unknown position the + // destination is emitted as a separate XY move followed by its own + // Z move, on top of the lift. That split is the pre-existing + // identity-mapping path and must not change. + REQUIRE(count_g1(gcode) == 3); + } + } + } +} + +SCENARIO("Axis remap: eager_lift does not lift, or record a lift, at an unknown position", + "[GCodeWriter][remap]") +{ + GIVEN("a writer with a reverse-X remap and an unknown current position") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(6, 1, 2); + writer.set_build_volume_max(Vec3d(250.0, 250.0, 250.0)); + REQUIRE_FALSE(writer.is_current_position_clear()); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("no move is emitted") { + REQUIRE(lift.empty()); + } + THEN("no lift is recorded, so unlift does not descend from it") { + // If m_lifted had been set while nothing was commanded, unlift() + // would emit a descent from a height the machine never reached. + REQUIRE(writer.unlift().empty()); + } + } + } + + GIVEN("an identity-mapping writer with an unknown position") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an eager lift is requested") { + const std::string lift = writer.eager_lift(LiftType::NormalLift); + + THEN("behaviour is unchanged: the lift is emitted and can be undone") { + REQUIRE_FALSE(lift.empty()); + REQUIRE_FALSE(writer.unlift().empty()); + } + } + } +} + +// Bug 2. extrude_arc_to_xy() emits G2/G3 with logical X/Y and I/J and never +// consulted the mapping. An arc is only representable when logical X and Y reach +// the machine unchanged -- which is a narrower question than "is the remap the +// identity", because a mapping that only touches Z leaves every emitted word alone. +SCENARIO("Arc support is decided by whether the mapping leaves X and Y alone", "[GCodeWriter][remap]") +{ + GIVEN("a Cartesian writer") { + GCodeWriter writer; + + THEN("the identity mapping supports arcs") { + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only negation still supports arcs") { + // (+X, +Y, -Z): non-identity, but X, Y, I and J are all untouched. + writer.set_axis_remap(0, 1, 5); + REQUIRE(writer.kinematics().must_emit_all_axes()); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("a Z-only reversal still supports arcs") { + writer.set_axis_remap(0, 1, 8); + REQUIRE(writer.kinematics().supports_arc_moves()); + } + THEN("swapping X and Y does not support arcs") { + writer.set_axis_remap(1, 0, 2); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + THEN("the X-tilt style (x, z, y) remap does not support arcs") { + writer.set_axis_remap(0, 2, 1); + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } + + GIVEN("a belt writer") { + PrintConfig belt_config; + belt_config.belt_printer.value = true; + belt_config.belt_slice_rotation.value = BeltRotationAxis::X; + belt_config.belt_slice_rotation_angle.value = 45.0; + + GCodeWriter writer; + install_belt_kinematics(writer, belt_config); + + THEN("arcs are never supported, because the frame shears") { + REQUIRE_FALSE(writer.kinematics().supports_arc_moves()); + } + } +} + +SCENARIO("An unrepresentable arc degrades to its chord rather than emitting a wrong G2/G3", + "[GCodeWriter][remap]") +{ + auto emitted_commands = [](const std::string &gcode) { + std::vector cmds; + GCodeReader reader; + reader.parse_buffer(gcode, [&cmds](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd().empty()) cmds.emplace_back(line.cmd()); + }); + return cmds; + }; + + GIVEN("an identity-mapping writer") { + GCodeWriter writer; + configure_lift_writer(writer); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("it is still a G3") { + const auto cmds = emitted_commands(gcode); + REQUIRE(cmds.size() == 1); + REQUIRE(cmds.front() == "G3"); + } + } + } + + GIVEN("a writer whose mapping swaps X and Y") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + + WHEN("an arc is extruded") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true); + + THEN("no arc is emitted; it is approximated with linear moves") { + const auto cmds = emitted_commands(gcode); + REQUIRE(! cmds.empty()); + for (const auto &c : cmds) + REQUIRE(c == "G1"); + } + } + } + + // The first version of this test used dE = 0 with force_no_extrusion, which + // hid a real bug: the capability check sat AFTER filament()->extrude(dE), so + // the fallback into extrude_to_xy() advanced E twice. Extrusion accounting has + // to be asserted with a positive dE. + GIVEN("a writer whose mapping cannot express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + writer.set_axis_remap(1, 0, 2); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("exactly dE is accounted for, not twice dE") { + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + THEN("no G2/G3 survives") { + REQUIRE(gcode.find("G2") == std::string::npos); + REQUIRE(gcode.find("G3") == std::string::npos); + } + } + } + + GIVEN("a writer whose mapping CAN express arcs, extruding a real amount") { + GCodeWriter writer; + configure_lift_writer(writer); + const double dE = 1.5; + // used_filament() accumulates across moves; E() is reset per line in + // relative-E mode, so it would only show the last segment. + const double used_before = writer.filament()->used_filament(); + + WHEN("an arc carrying that extrusion is emitted") { + const std::string gcode = writer.extrude_arc_to_xy( + Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false); + + THEN("it is still a single arc and accounts for dE once") { + REQUIRE(emitted_commands(gcode).size() == 1); + REQUIRE_THAT(writer.filament()->used_filament() - used_before, + Catch::Matchers::WithinAbs(dE, 1e-6)); + } + } + } +} From 767db71500ddd3098baf758adcf78a60bb0b4c9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:58:47 -0500 Subject: [PATCH 03/75] Belt: fix three tree-support bugs, one of which blocked slicing entirely 1. Belt tree support could not slice at all. layer_initialize() hardcodes layer 0's bottom_z to 0, encoding "below layer 0 is the build plate at z = 0". True for a flat bed; false for a belt, whose virtual support layers legitimately extend below zero. The bottom-most belt layer therefore got height = print_z - 0 = -9.8, which reached Flow::with_height() and threw FlowErrorNegativeFlow. A 3DBenchy, a mushroom, an L-bracket and an extruded L all failed identically with negative flow / return -100. Only a bare cube sliced, because its support never reached that far down. The bottom is now taken from the previous layer's z, and only a layer 0 whose print_z is itself negative gets a synthesised bottom below it. Every non-negative print_z -- every non-belt configuration -- keeps exactly the previous 0, so this is behaviour-preserving off a belt by construction. An earlier form used min(0., layer_z(0) - layer_height), which regressed flat beds whenever the initial layer was thinner than the layer height. 3DBenchy on a 45-degree belt with organic tree support: fails to slice -> 247 support blocks / 168,596 extrusions. 2. Support generated against the belt, and against belt-tilted walls. A plain 20mm cube on a 45-degree belt generated 86 support blocks and 46,307 support extrusions. Three causes, all gated on the belt floor being active: a. The build-plate tilt compensation shifted the lower layer the wrong way. tan(build_plate_tilt_*) carries a magnitude but no direction, and the sign chosen moved the lower layer away from the newly appearing material rather than under it, doubling the mismatch. The shift now comes from belt_floor_shear_factor / belt_floor_from_axis, which carry sign and axis exactly. Non-belt tilted beds keep the previous behaviour. b. Material resting on the belt was treated as unsupported. The belt surface is now unioned into the effective lower layer, sampled at the bottom of the layer -- a layer meets the belt across its thickness and print_z is the top. The half-plane is clipped to the layer's bounding box first: unioning a +/-1000mm half-plane raw with 20mm-scale geometry put a huge dynamic range through Clipper and left intermittent artefacts every few layers. c. The object's first slice can be empty on a belt (the bottom vertex is a sub-extrudable sliver), leaving the layer above with an empty predecessor even though it rests on the belt. (b) already covers that per island. What did need fixing is sharp-tail detection, which tests each island against the raw lower slices; with an empty predecessor that test is trivially true and every belt-contact island read as a sharp tail. It now tests against the same effective lower layer. An earlier form instead skipped the whole layer when the point of get_extents(curr_polys) -- the bounding box of the union of every island -- nearest the belt was in contact. That was wrong in a way worth recording: one island resting on the belt could suppress overhang and sharp-tail detection for a separate island floating well above it. Every decision here is per-island. Cube on belt: 46,307 -> 0 support extrusions. Same cube non-belt: 0 before and after. Benchy on belt still 247 blocks / 168,596 extrusions and a mushroom 111 / 82,157, so false positives are removed without suppressing true ones. Non-belt is unchanged by measurement, not only by the belt_ovh_active gate: the same mushroom sliced on a Cartesian printer before and after gives 65,866 support extrusions and 68,717 total extrusions both times, the two G-code files differing in exactly one line -- the object's plate position. 3. m_anti_overhang was filled and read in different index spaces. It is consumed in the same index space as m_layer_outlines, where object layer i lives at num_raft_layers + i, but was filled in object-layer space. Every entry landed num_raft_layers too low (50 for a 20mm cube at bed Y=50) and the topmost object layers got none. The belt injection also ran before m_raft_layers was extended, so it could not have known the offset. The array is now shifted as a whole and the injection moved after the raft extension. This also repairs user support blockers under a raft, which is not belt-specific: it changes behaviour for any ordinary raft, not just the belt's virtual one, and should be reviewed as a general fix. Measured effect on the cube was small on its own (46,307 -> 46,334 before the other fixes) because m_anti_overhang only feeds calculate_placable; kept as a correctness fix on its own merits. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/Support/TreeModelVolumes.cpp | 55 ++++++++------- src/libslic3r/Support/TreeSupport.cpp | 75 +++++++++++++++++++-- src/libslic3r/Support/TreeSupportCommon.hpp | 17 ++++- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index b1e3cdd6dd..e56682fd3c 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -95,29 +95,6 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); - // Belt floor: add belt surface polygons to anti_overhang so support - // is never generated inside the belt. Only in global shear mode — - // in local mode the belt floor clipping handles everything and - // anti_overhang at the bottom layers would block all support. - { - const auto &sp = print_object.slicing_parameters(); - const auto &pcfg = print_object.print()->config(); - BeltFloorContext ctx; - ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); - if (ctx.is_active() - && std::abs(print_object.belt_global_z_offset()) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - size_t num_layers_needed = print_object.layer_count(); - // Ensure m_anti_overhang is large enough. - if (m_anti_overhang.size() < num_layers_needed) - m_anti_overhang.resize(num_layers_needed, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { - double print_z = print_object.get_layer(layer_idx)->print_z - - print_object.belt_global_z_offset(); - append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); - } - } - } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -147,6 +124,38 @@ TreeModelVolumes::TreeModelVolumes( } } } + // Belt floor: add belt surface polygons to anti_overhang so support is + // never generated inside the belt. + // + // This MUST run after m_raft_layers is final. m_anti_overhang is consumed + // in the same index space as m_layer_outlines -- object layer i lives at + // index num_raft_layers + i -- but slice_support_blockers() returns it in + // object-layer space. Without the shift below, every entry lands + // num_raft_layers too low: with the belt raft that is tens of layers, so + // the belt suppression is applied to the wrong layers entirely and the + // topmost object layers get none at all. + { + const size_t num_raft = m_raft_layers.size(); + const size_t num_obj = print_object.layer_count(); + if (num_raft > 0 && ! m_anti_overhang.empty()) + // Shift the support blockers into the same space. + m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{}); + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); + if (ctx.is_active() + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + if (m_anti_overhang.size() < num_raft + num_obj) + m_anti_overhang.resize(num_raft + num_obj, Polygons{}); + for (size_t i = 0; i < num_obj; ++i) { + const double print_z = print_object.get_layer(i)->print_z + - print_object.belt_global_z_offset(); + append(m_anti_overhang[num_raft + i], ctx.surface_polygon(print_z)); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 3b7743407c..d472681c41 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -13,6 +13,7 @@ #include "SVG.hpp" #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" +#include #include "TreeSupport3D.hpp" #include "BeltFloorContext.hpp" #include @@ -712,6 +713,17 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + + // Belt printers: the object is pre-rotated by the belt angle before slicing, so a wall + // that is vertical in the world advances by one layer height per layer in the sliced + // frame. The build-plate tilt shift above compensates for that, but its direction has to + // follow the belt shear -- the sign and axis are already known exactly from the slicing + // parameters, so take them from there rather than from tan(build_plate_tilt), which + // carries a magnitude but no direction. Non-belt tilted beds keep the existing behaviour. + BeltFloorContext ovh_belt_ctx; + const bool belt_ovh_active = ovh_belt_ctx.init(m_slicing_params, print_cfg); + const double belt_shear = ovh_belt_ctx.shear_factor(); + const int belt_axis = ovh_belt_ctx.from_axis(); // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -855,15 +867,61 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& lower_polys = lower_layer->lslices_extrudable; // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + // + // On a belt the object's very first slice can come out empty (the bottom + // vertex is a sub-extrudable sliver), leaving the layer above it with an + // empty predecessor even though it rests on the belt. That case needs no + // special handling here: the belt surface is unioned into effective_lower + // below and sampled at the bottom of the layer, so a contacting island is + // covered and a genuinely floating one still reports its overhang. Doing it + // that way keeps the decision per-island -- an earlier whole-layer skip, + // conditioned on the nearest point of the *union* of the cross-section, + // let one contacting island silence a separate floating one. ExPolygons shifted_lower; - if (has_tilt) { + if (belt_ovh_active || has_tilt) { shifted_lower = lower_polys; // copy const double lh = lower_layer->height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); + Point tilt_shift(0, 0); + if (belt_ovh_active) { + // Advance the lower layer along the belt by exactly the amount a + // world-vertical wall moves per layer, so such a wall stops reading + // as an overhang. Sign comes from the shear, not from a tilt angle. + const coord_t d = coord_t(-scale_(lh * belt_shear)); + if (belt_axis == 0) tilt_shift.x() = d; else tilt_shift.y() = d; + } else { + tilt_shift = Point(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + } translate(shifted_lower, tilt_shift); } - const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + ExPolygons effective_lower = (belt_ovh_active || has_tilt) ? shifted_lower : lower_polys; + + // Belt printers: material resting on the belt is held up by the belt, not by + // the layer below it, so the belt surface counts as support from underneath. + // Without this the object's belt-contact face reads as a fresh overhang on + // every layer -- the leading strip that produced the spurious support nub. + if (belt_ovh_active) { + // surface_polygon() is a +/-1000mm half-plane. Unioning that raw with + // 20mm-scale geometry and then offsetting it puts a huge dynamic range + // through Clipper, which left intermittent artefacts every few layers. + // Clip it to the layer's own bounding box first. + // Evaluate the belt surface at the BOTTOM of the layer, not its top: + // a layer meets the belt across its whole thickness, and print_z is the + // top. On the object's first layer -- which is thicker, and whose lower + // layer is empty -- using print_z left the leading 0.37mm uncovered and + // produced the one remaining spurious overhang. + Polygons belt_surface = ovh_belt_ctx.surface_polygon(layer->print_z - layer->height); + if (! belt_surface.empty()) { + BoundingBox clip_bb = get_extents(curr_polys); + clip_bb.merge(get_extents(lower_polys)); + clip_bb.offset(scale_(10.)); + belt_surface = intersection(belt_surface, Polygons{ clip_bb.polygon() }); + if (! belt_surface.empty()) { + append(effective_lower, union_ex(belt_surface)); + effective_lower = union_ex(effective_lower); + } + } + } // normal overhang ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); @@ -882,8 +940,13 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) for (const ExPolygon& expoly : curr_polys) { bool is_sharp_tail = false; // 1. nothing below - // this is a sharp tail region if it's floating and non-ignorable - if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) { + // this is a sharp tail region if it's floating and non-ignorable. + // On a belt, "below" has to include the belt itself and the + // shear-advanced lower layer, or every belt-contact island reads as + // a sharp tail -- which is what the empty-predecessor skip above was + // really masking. effective_lower is exactly that notion of below. + const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; + if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) { is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty(); } diff --git a/src/libslic3r/Support/TreeSupportCommon.hpp b/src/libslic3r/Support/TreeSupportCommon.hpp index 07dcb324b8..979adcd9ed 100644 --- a/src/libslic3r/Support/TreeSupportCommon.hpp +++ b/src/libslic3r/Support/TreeSupportCommon.hpp @@ -651,7 +651,22 @@ inline SupportGeneratorLayer& layer_initialize( const size_t layer_idx) { layer_new.print_z = layer_z(slicing_params, config, layer_idx); - layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0; + // Layer 0 has no layer below it, so its bottom is the build plate at z = 0 -- + // true for a flat bed, false for a belt, whose virtual support layers extend + // below zero. Taking 0 there made the bottom-most belt layer's height come out + // as its own (negative) print_z, which reached Flow::with_height() and threw + // FlowErrorNegativeFlow, so tree support could not slice any belt model whose + // branches reached down that far. + // + // Only the negative case is corrected. An earlier version used + // min(0, print_z - layer_height), which also fires whenever the initial layer + // is THINNER than the regular layer height -- e.g. 0.2 over 0.3, both + // independently configurable -- and silently changed flat-bed support layer + // heights. Keying on the sign leaves every non-negative print_z on exactly + // the previous value of 0. + layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.; + if (layer_idx == 0 && layer_new.print_z < 0.) + layer_new.bottom_z = layer_new.print_z - slicing_params.layer_height; layer_new.height = layer_new.print_z - layer_new.bottom_z; return layer_new; } From 1b6fb2a81f2c14fc1ddce598d0a9dd2897d3ce9f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:00:38 -0500 Subject: [PATCH 04/75] Belt: fix first-layer speed and the slow_down_layers ramp never applying Fixes the report in #12998 (comment 5465250754): first-layer speed and the slow_down_layers ramp were ignored on a belt printer. The report reads as a per-object problem, but neither applied to *any* object -- the reporter's first part slowed down because slow_down_for_layer_cooling was on, which is CoolingBuffer's time-per-layer mechanism, not initial_layer_speed. FirstLayerPlane decides first-layer-ness by perpendicular distance to a plane it derives by composing gcode_remap_* with compute_machine_z_affine(). The plane is therefore a function of how G-code is *addressed*, not of where the belt is: change the output axis convention and the plane moves. On MCBELT-TYPE2 the first layer measured 86.2 mm from the plane and got effective index 431, far past any slow_down_layers ramp. on_first_layer(point) and effective_layer_index_for_point() now measure height above the belt surface, using the belt description already carried in SlicingParameters -- belt_floor_shear_factor / belt_floor_from_axis / belt_floor_z_shift -- the same description the support generator uses. That is a property of how the object was sliced, so no remap or back-transform can perturb it. Deliberately not via BeltFloorContext: its init() folds in belt_support_floor_offset, a support-generator diagnostic, and letting that option steer the model's first-layer speed band would be a surprising coupling (a negative value would switch the slowdown off outright). Preserving the existing first-layer-plane settings: * first_layer_plane XY/YZ/XZ keeps the FirstLayerPlane evaluator, as those are explicit opt-outs. * A non-zero first_layer_plane_offset also keeps it. The offset is a machine-Z shift that FirstLayerPlane converts into a perpendicular distance in the slicing frame; this evaluator measures along slicing Z, so there is no faithful translation. Deferring to the evaluator that implements the setting beats silently ignoring it. * The two thresholds stay separate, exactly as FirstLayerPlane keeps them: the first-layer boolean tests initial_layer_print_height, while the effective layer index counts bands of first_layer_plane_thickness. Brim and coincident apron bands are emitted before m_layer is switched to their object -- for an apron band there is no Layer at all -- so both paths publish the belt-floor owner explicitly. Without that a brim's classification would borrow whichever object was visited previously, making it depend on plate order. Note that first-layer-ness drives more than speed: extrusion acceleration, jerk, the first-layer flow ratio and eligibility for overhang speed/fan analysis all read it, so all of them are corrected on belt printers by this change. Classification still samples only each path's first point, as it did before. Non-belt is unaffected by construction: belt_height_above_floor() returns false when the belt floor is inactive and both call sites fall back to the previous path. FirstLayerPlane stays in place for its other modes and for CoolingBuffer, whose machine-coordinate probe is a separate outstanding bug. Measured, MCTEST4 on MCBELT-TYPE2 (initial_layer_speed=5, slow_down_layers=40): 15 distinct feedrates with no gradient and F300 absent, becomes 70 including the full ramp 300(5) 382(6) 465(8) 630(10) 795(13) ... Two bare cubes on a belt: 0 slow extrusions becomes 2378 across Z 32.36..95.18. The same two cubes on a Cartesian printer keep their slow extrusions confined to Z 0.20..2.00. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/GCode.cpp | 50 +++++++++++++++++++++++++++++++++++++++++ src/libslic3r/GCode.hpp | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 32976fad76..fff437731f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6,6 +6,7 @@ #include "libslic3r.h" #include "I18N.hpp" #include "GCode.hpp" +#include #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" @@ -5234,6 +5235,10 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o // geometry is already in plate coordinates. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // m_layer is not switched to this object until after brim emission, so name + // the belt-floor owner explicitly or the classification borrows whichever + // object was visited last. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const Point &offset = object.instances()[instance_id].shift; this->set_origin(unscale(offset)); this->on_set_origin(&object, offset); @@ -6642,6 +6647,9 @@ LayerResult GCode::process_layer( // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); + // Apron bands have no Layer at all (m_layer is null here), so the belt + // floor owner has to be named the same way the object brim names it. + BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object }; const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() : single_object_instance_idx + 1; @@ -10307,6 +10315,48 @@ std::string GCode::set_object_info(Print *print) { return gcode.str(); } +bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const +{ + // The owning object, which is what carries the belt description. During + // object-brim and coincident-apron emission m_layer still points at whichever + // object was visited last (or at nothing at all), so those paths publish the + // owner explicitly -- otherwise a brim's speed would depend on plate order. + const PrintObject *object = m_belt_floor_object != nullptr ? m_belt_floor_object + : (m_layer != nullptr ? m_layer->object() : nullptr); + if (object == nullptr) + return false; + // Respect an explicit first-layer-plane choice: only Auto and BeltAffine mean + // "use the belt". A user who selected XY, YZ or XZ has asked for the + // FirstLayerPlane evaluator and must keep it. + const FirstLayerPlaneMode mode = m_config.first_layer_plane.value; + if (mode != FirstLayerPlaneMode::Auto && mode != FirstLayerPlaneMode::BeltAffine) + return false; + // Likewise for a dialled-in plane offset. It is expressed as a machine-Z + // shift that FirstLayerPlane converts into a perpendicular distance in the + // slicing frame; this evaluator measures along slicing Z instead, so there is + // no faithful translation of it here. Honour the user's setting by deferring + // to the evaluator that implements it rather than silently dropping it. + if (std::abs(m_config.first_layer_plane_offset.value) > EPSILON) + return false; + + const SlicingParameters &sp = object->slicing_parameters(); + // Deliberately NOT BeltFloorContext: its init() folds in + // belt_support_floor_offset, a support-generator diagnostic. Letting that + // option move the model's first-layer speed band would be a surprising + // coupling -- a negative value would switch the slowdown off entirely. + // The belt surface itself is just shear * u + z_shift. + if (std::abs(sp.belt_floor_shear_factor) < EPSILON) + return false; + const double u = sp.belt_floor_from_axis == 0 ? point_slicing_mm.x() : point_slicing_mm.y(); + const double floor_z = sp.belt_floor_shear_factor * u + sp.belt_floor_z_shift; + // Measured along the slicing Z, not perpendicular to the belt: layers are + // horizontal slabs in the sliced frame, so the slab holding the material that + // rests on the belt at this point is the one within one layer height of it. + // A perpendicular measure would shrink the band by 1/cos(tilt). + height_mm = point_slicing_mm.z() - floor_z; + return true; +} + // convert a model-space scaled point into G-code coordinates Vec2d GCode::point_to_gcode(const Point &point) const { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 26b8bc78a5..c78a9675e2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -831,6 +831,18 @@ protected: // Counter standing in for Layer::id() on apron layers, which precede layer 0. size_t m_belt_brim_layer_idx{0}; + // Belt brim only. Brim and coincident apron bands are emitted before m_layer + // is switched to their object, so belt_height_above_floor() would otherwise + // read the previously visited object's belt description -- making a brim's + // classification depend on plate visiting order. Those paths publish the + // owner here for the duration of the emission. Never left set. + const PrintObject *m_belt_floor_object{nullptr}; + struct BeltFloorObjectGuard { + const PrintObject *&slot; + BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; } + ~BeltFloorObjectGuard() { slot = nullptr; } + }; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS @@ -853,6 +865,12 @@ protected: // otherwise we delegate to the legacy per-layer test. This is the // entry point used by per-path call sites in _extrude. bool on_first_layer(const Vec3d &point_slicing_mm) const { + // Belt printers: measure height above the belt surface itself, in the + // slicing frame. See belt_height_above_floor() for why this does not go + // through FirstLayerPlane. + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) + return h <= m_config.initial_layer_print_height.value + EPSILON; if (m_first_layer_plane && m_first_layer_plane->is_active()) return m_first_layer_plane->is_first_layer( point_slicing_mm, m_config.initial_layer_print_height.value); @@ -863,10 +881,40 @@ protected: // perpendicular distance to the plane in band_thickness_mm units; // otherwise it returns the legacy slicing layer index. int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const { + double h; + if (this->belt_height_above_floor(point_slicing_mm, h)) { + const double lh = this->first_layer_band_mm(); + return h <= 0. ? 0 : int(std::floor(h / lh)); + } if (m_first_layer_plane && m_first_layer_plane->is_active()) return m_first_layer_plane->effective_layer_index(point_slicing_mm); return on_first_layer() ? 0 : layer_id(); } + + // Band thickness for the *effective layer index* only. FirstLayerPlane keeps + // two separate thresholds and so must this path: is_first_layer() tests + // against initial_layer_print_height, while effective_layer_index() counts + // bands of first_layer_plane_thickness. Conflating them would apply + // first-layer treatment through a whole 1mm band on a 0.2mm first layer. + double first_layer_band_mm() const { + double band = m_config.first_layer_plane_thickness.value; + if (band <= 0.) band = m_config.initial_layer_print_height.value; + return band > 0. ? band : 0.2; + } + + // Height of a slicing-frame point above the belt surface, or false when this + // is not a belt print. + // + // The belt surface is known exactly in the slicing frame from the slicing + // parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same + // description the support generator uses. FirstLayerPlane instead derives its + // plane by composing gcode_remap_* with the g-code back-transform, so its + // answer changes with the machine's *output* axis convention: on a printer + // with a non-identity remap it reported ~86mm of clearance for geometry + // sitting directly on the belt, and no extrusion was ever classified as + // first-layer. Measuring against the belt itself is independent of every + // remap and back-transform. + bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const; int layer_id() const { if (m_layer == nullptr) return -1; From 1bcfe58064ea16387a3f4107d7ea782fc43c6e9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH 05/75] Belt: stop the purge prism printing plastic no toolchange needs Two independent leaks of filament on the belt purge prism, plus the replan safety net the second one needs. 1. The early-truncation scan bounded itself with the prism's own toolchanges. ToolOrdering covers the whole print and the prism is a printed object in it, so the "last toolchange" the scan found was on the prism's own top layers -- it runs past every model object by design -- and the truncation cancelled nothing. Bound the scan at the tallest non-prism object (support layers included; on a belt they can top the object). On MCTEST5 that was 197 toolchanges over 39.4 mm of tower that no swap ever needed. 2. On a layer with no toolchange, the prism's entire fill printed as solid infill in its own filament. Drop the fills no toolchange claimed, right after the purge marking and before ensure_perimeters_infills_order() force-overrides whatever is left. Perimeters stay so the bar keeps a continuous wall. An earlier version of this deleted the entities and had to be reverted: psWipeTower can rerun without regenerating infill, and a later tool ordering may claim what this one did not. The entities are now stashed with their layer, region and index and put back exactly, the same reversibility contract layer truncation already had. 3. Both stashes go stale if an object step reruns: make_fills() clears and regenerates fills over m_layers only, so a stale stash would put old fills back next to new ones, and truncated layers would keep old perimeters/fills. Undo the plan's edits at the top of Print::process() whenever psWipeTower is not done. Every object-step invalidation also invalidates psWipeTower, so that condition is exactly "some object step may rerun"; when it is done nothing regenerates and the edits must stay. This also covers a prism left behind after belt mode is turned off, which previously stayed truncated forever. WipingExtrusions::is_entity_overridden() becomes public so the prism can tell claimed fills from unclaimed ones. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/libslic3r/BeltPurge.cpp | 109 +++++++++++++++++++++++++-- src/libslic3r/GCode/ToolOrdering.hpp | 8 +- src/libslic3r/Print.cpp | 13 ++++ src/libslic3r/Print.hpp | 32 ++++++++ src/libslic3r/PrintObject.cpp | 7 ++ 5 files changed, 162 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index b249f21bd2..37d89ca14e 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -149,7 +149,7 @@ void Print::_plan_belt_purge() // previous plan so a newly higher toolchange can use its original layers. for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value) - po->belt_restore_truncated_layers(); + po->belt_undo_purge_plan(); // Must run before ToolOrdering is built: LayerTools merge per-object layer // print_z values, and the prism only absorbs purge where its (snapped) @@ -191,11 +191,44 @@ void Print::_plan_belt_purge() // flush object), so truncating afterwards would leave dangling overrides // pointing into deleted layers. { + // The tool ordering covers the WHOLE print, and the prism is a printed + // object in it. Left unbounded, the scan below sees the prism's own + // toolchanges on layers above every model object -- the prism runs past + // them by design (ramp/height compensation at the tilted ends) -- so + // last_tc_z lands at the prism's own top and the truncation cancels + // nothing. The tower ends up justifying its own existence. + // + // Nothing above the tallest printed object can require a color change, + // so bound the scan there. On MCTEST5 that is 197 toolchanges spanning + // z=154.00..193.20 with the tallest object topping out at 153.80, i.e. + // 39.4 mm of tower that no swap ever needed. + // Support layers count too: on a belt they can extend above the object's + // own top, and a toolchange there is a real one. + double obj_top_z = -1.; + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value) + continue; + if (!po->layers().empty()) + obj_top_z = std::max(obj_top_z, po->layers().back()->print_z); + if (!po->support_layers().empty()) + obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z); + } + double last_tc_z = -1.; unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); - for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) { + // layer_tools() is ordered by print_z ascending. + if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON) + break; for (const unsigned int e : lt.extruders) if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + } + // Deliberately NOT cancelling the prism outright when no object toolchange + // exists: belt_truncate_layers_above(0.) empties m_layers, and an object + // with zero layers is not something the rest of the pipeline expects. The + // GUI already declines to create a prism unless more than one filament is + // in use, so this case is a stale prism, not a hot path -- leave it whole + // rather than risk a zero-layer object. if (last_tc_z >= 0.) for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { @@ -251,9 +284,25 @@ void Print::_plan_belt_purge() current_extruder_id = extruder_id; } - // Do not destructively remove unclaimed fill entities here. psWipeTower - // can rerun without regenerating infill, and a later tool ordering may - // need entities that were unclaimed by the previous plan. + // Plastic saving: drop the prism's fills that no toolchange on this layer + // claimed. At this point the prism's OVERRIDDEN fills are exactly the + // purge; the rest would print as solid infill in the prism's own filament + // for nothing -- which is the whole prism on a layer with no toolchange + // (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are + // left alone so the bar keeps a continuous wall along the belt. + // + // Non-destructive: the entities are stashed with their positions and put + // back by belt_restore_dropped_fills() at the top of the next plan. An + // earlier version deleted them outright, which broke replanning when a + // later tool ordering needed what this one had not claimed -- that is why + // it was removed rather than kept. + if (prism_po != nullptr) { + const auto &we = layer_tools.wiping_extrusions(); + prism_po->belt_drop_unclaimed_fills( + prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON), + [&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); }); + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (layer_leftover > 0.f) { total_leftover += layer_leftover; @@ -330,6 +379,56 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) return removed; } +// Plastic saving on the purge prism: keep only the fills a toolchange claimed. +// +// Called per layer from _plan_belt_purge(), after the real-purge marking and +// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every +// remaining fill on the prism (it is a dedicated flush object), so afterwards +// everything looks claimed and nothing could be distinguished. +size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed) +{ + if (layer == nullptr) + return 0; + size_t dropped = 0; + for (size_t ri = 0; ri < layer->regions().size(); ++ri) { + LayerRegion *lr = layer->get_region(ri); + auto &ents = lr->fills.entities; + ExtrusionEntitiesPtr keep; + keep.reserve(ents.size()); + for (size_t i = 0; i < ents.size(); ++i) { + if (claimed(ents[i])) { + keep.emplace_back(ents[i]); + } else { + // Stash with its original index so the restore is exact. + m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] }); + ++dropped; + } + } + ents = std::move(keep); + } + return dropped; +} + +void PrintObject::belt_restore_dropped_fills() +{ + if (m_belt_dropped_fills.empty()) + return; + // Ascending index per (layer, region): inserting in that order lands every + // entity back at its original position, because each insertion shifts only + // the entries after it, which are themselves still to be inserted. + std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(), + [](const BeltDroppedFill &a, const BeltDroppedFill &b) { + if (a.layer != b.layer) return a.layer < b.layer; + if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx; + return a.index < b.index; + }); + for (const BeltDroppedFill &d : m_belt_dropped_fills) { + auto &ents = d.layer->get_region(d.region_idx)->fills.entities; + ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity); + } + m_belt_dropped_fills.clear(); +} + void PrintObject::belt_restore_truncated_layers() { if (m_belt_truncated_layers.empty()) diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f38cb4bdc8..9ccf6e90fb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -74,13 +74,17 @@ public: void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; } -private: - // Returns true if entity is not printed with its usual extruder for a given copy. + // Returns true if entity is not printed with its usual extruder for a given + // copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the + // belt purge prism uses it to tell which of its fills actually carry purge + // from the ones that are unclaimed waste (Print::_plan_belt_purge()). bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { auto it = entity_map.find(std::make_tuple(entity, object)); return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; } +private: + int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 86272e612c..1fc92aa9fc 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2490,6 +2490,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) if (m_objects.empty()) return; + // Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's + // layers and drops its unclaimed fills, stashing both so a replan can undo + // them. The object steps below regenerate per-layer content over m_layers + // ONLY, so if any of them is about to rerun the stashes must go back first; + // otherwise truncated layers keep stale perimeters/fills and dropped fills + // are re-inserted next to freshly generated ones. Every object-step + // invalidation also invalidates psWipeTower, so "psWipeTower not done" is + // exactly "some object step may rerun" -- and when it IS done nothing below + // regenerates, and the plan's edits have to stay. + if (!this->is_step_done(psWipeTower)) + for (PrintObject *obj : m_objects) + obj->belt_undo_purge_plan(); + for (PrintObject *obj : m_objects) obj->clear_shared_object(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f3da337bb7..638bb93105 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -582,6 +582,28 @@ private: // Wipe-tower-only invalidations do not necessarily reslice the object, so // truncation must be reversible when later toolchanges move upward. void belt_restore_truncated_layers(); + // Belt purge prism, plastic saving: drop the fills on one layer that no + // toolchange claimed. `claimed` reports whether an entity was overridden as + // purge; everything else on that layer would otherwise print as solid infill + // in the prism's own filament for nothing. Perimeters are never touched, so + // the bar keeps a continuous wall along the belt. + // + // Entities are STASHED, not deleted, with their original positions -- the + // same reversibility contract belt_truncate_layers_above() has, and the + // reason the original version of this had to be removed: psWipeTower can + // rerun without regenerating infill, and a later tool ordering may claim what + // this one did not. Returns the number of entities dropped. + size_t belt_drop_unclaimed_fills(Layer *layer, const std::function &claimed); + // Put every stashed fill back at its original index. Must run before a replan. + void belt_restore_dropped_fills(); + // Undo every edit _plan_belt_purge() made to this object's layers, leaving + // m_layers exactly as the object steps produced it. Fills first: they point + // into layers that are still live, and truncated layers were stashed whole + // with their own fills untouched, so the two stashes never share an entity. + // Print::process() calls this before any object step may rerun (those steps + // regenerate per-layer content over m_layers only, so a stale stash would + // otherwise be restored on top of fresh content); the plan calls it too. + void belt_undo_purge_plan() { belt_restore_dropped_fills(); belt_restore_truncated_layers(); } //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -625,6 +647,16 @@ private: SlicingParameters m_slicing_params; LayerPtrs m_layers; LayerPtrs m_belt_truncated_layers; + // Fills removed by belt_drop_unclaimed_fills(), owned by this vector until + // restored or until clear_layers() deletes them. An entity is in exactly one + // of the live collection or this stash, never both. + struct BeltDroppedFill { + Layer *layer { nullptr }; + size_t region_idx { 0 }; + size_t index { 0 }; // position in the original fills.entities + ExtrusionEntity *entity { nullptr }; + }; + std::vector m_belt_dropped_fills; SupportLayerPtrs m_support_layers; // Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local // slicing frame, one entry per object layer plus a prologue of brim-only diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 6d9bfba090..2f02588d37 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1119,6 +1119,13 @@ void PrintObject::clear_layers() for (Layer *l : m_belt_truncated_layers) delete l; m_belt_truncated_layers.clear(); + // Fills dropped for plastic saving are owned by the stash while they sit + // outside their layer's collection, so they are freed here too. Order + // matters only in that these point at layers deleted just above, and we + // never dereference the layer -- just the entity. + for (const BeltDroppedFill &d : m_belt_dropped_fills) + delete d.entity; + m_belt_dropped_fills.clear(); } } From a43043069075b56c4a1bcbdc9d8b22217b3eb936 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH 06/75] Belt: size the purge prism against physical filaments, not mixed slots A mixed filament slot is virtual: ToolOrdering::resolve_mixed_filaments() replaces it with its physical components before any G-code is emitted, so the toolchanges the prism has to absorb are between those components. ensure_belt_purge_tower() counted the slot as a filament of its own, provisioning one island per mixed slot that no swap can ever reach -- the "extra purge tower" on MCTEST5, where filament 5 is a 50/50 blend of 2 and 4 and the G-code reports 0.00 g of it used. Expand the assigned set with the same expand_mixed_filaments() the backend uses, so the GUI sizes the prism against the filament set the slicer actually produces. No-op when nothing is mixed. Test covers the MCTEST5 shape, a mixed slot whose components are otherwise unused, and the no-mixing case. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/slic3r/GUI/BeltPurgeTower.cpp | 34 +++++++++++++++++++ tests/libslic3r/test_filament_mixer.cpp | 44 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 8c5764072e..1882363d27 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -9,6 +9,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/FilamentMixer.hpp" #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Geometry.hpp" #include "libslic3r/BoundingBox.hpp" @@ -131,6 +132,39 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } } + // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time + // ToolOrdering::resolve_mixed_filaments() replaces it with its physical + // components, so the toolchanges the prism has to absorb are between those + // components, not to the mixed slot itself. Counting the slot as a filament + // of its own therefore over-provisions the prism by one island per mixed slot + // -- the "extra purge tower" -- and, when every component is already used by + // another object, by an island that can never be reached at all. + // + // Expand here with the same helper the backend uses (Print.cpp's sequential + // path), so the GUI sizes the prism against the same filament set the slicer + // will actually produce. No-op when no filament is mixed. + { + // Copies, not references: a ternary with an empty-vector fallback would bind + // a reference to a temporary. + std::vector is_mixed; + std::vector comp_strs; + if (const auto *o = full_cfg.option("filament_is_mixed")) + is_mixed = o->values; + if (const auto *o = full_cfg.option("filament_mixed_components")) + comp_strs = o->values; + if (has_any_mixed_filament(is_mixed)) { + std::vector zero_based; + zero_based.reserve(filaments.size()); + for (int f : filaments) + if (f > 0) + zero_based.push_back((unsigned int) (f - 1)); + zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); + filaments.clear(); + for (unsigned int f : zero_based) + filaments.insert((int) f + 1); + } + } + const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; if (!wanted) { sig = BeltPurgeSignature{}; diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp index ade0c910dc..31bcb09526 100644 --- a/tests/libslic3r/test_filament_mixer.cpp +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -57,6 +57,50 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", " } } +TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]") +{ + // Regression for the "extra purge tower" on a belt printer with a mixed + // filament (MCTEST5). The belt purge prism is sized as + // n_islands = used_filaments.size() - 1 + // and GUI::ensure_belt_purge_tower() collected those filaments straight off + // the model objects' extruder assignments. A mixed slot is VIRTUAL -- no + // nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it + // with its components before any G-code is emitted -- so counting it as a + // filament of its own provisions one island that can never be reached. + // + // MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of + // filaments 2 and 4. The G-code uses only T0..T3 and reports + // "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5 + // consumes nothing, exactly as a virtual slot should. + const std::vector is_mixed = {0, 0, 0, 0, 1}; + const std::vector comp_strs = {"", "", "", "", "2,4"}; + + // The set the sizer used to see: slots 0..4 (filaments 1..5). + const std::vector assigned = {0, 1, 2, 3, 4}; + const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs); + + // Slot 4 dissolves into 1 and 3, which are already present. + REQUIRE(physical == std::vector({0, 1, 2, 3})); + + // Four physical filaments => three transitions => three islands, not four. + REQUIRE(int(physical.size()) - 1 == 3); + REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix + + SECTION("A mixed slot whose components are otherwise unused still counts them") { + // Only the mixed slot is assigned: it must still yield its two components, + // i.e. one island, rather than collapsing to zero. + const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs); + REQUIRE(only_mixed == std::vector({1, 3})); + REQUIRE(int(only_mixed.size()) - 1 == 1); + } + + SECTION("No mixed filaments anywhere leaves the set untouched") { + const std::vector none_mixed = {0, 0, 0, 0, 0}; + REQUIRE_FALSE(has_any_mixed_filament(none_mixed)); + REQUIRE(expand_mixed_filaments(assigned, none_mixed, {"", "", "", "", ""}) == assigned); + } +} + TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]") { const std::vector is_mixed = {0, 0, 1}; From 9c83631d2061091eab5c2a0d70607a2195071ba8 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 12 Sep 2026 22:23:14 -0500 Subject: [PATCH 07/75] TreeSupport: drop the include left over from removed debug output Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ --- src/libslic3r/Support/TreeSupport.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index d472681c41..0b8eed9f6b 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -13,7 +13,6 @@ #include "SVG.hpp" #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" -#include #include "TreeSupport3D.hpp" #include "BeltFloorContext.hpp" #include From 6873267a9cb79f0829048983f662a1fedf54d6a7 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:56:00 +0800 Subject: [PATCH 08/75] Remove Plate Tilt Keys from Per-Object Settings Tables build_plate_tilt_x/y are printer-preset keys; listing them in the per-object frequent-settings and object-table bundles stored ignored values in object configs and crashed the object table on the process config lookup. --- src/slic3r/GUI/GUI_Factories.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 30fdfc0d04..6a47d5ad2f 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -63,7 +63,6 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF = { L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap", "support_base_pattern", "support_on_build_plate_only","support_critical_regions_only", "support_remove_small_overhang", - "build_plate_tilt_x", "build_plate_tilt_y", "support_base_pattern_spacing", "support_expansion"}}, //BBS { L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} } @@ -93,8 +92,7 @@ std::map> SettingsFactory::OBJECT_C {"support_bottom_z_distance", "",22},{"support_top_z_distance", "",23},{"support_base_pattern", "",24},{"support_base_pattern_spacing", "",25}, {"support_interface_top_layers", "",26},{"support_interface_bottom_layers", "",27},{"support_interface_spacing", "",28},{"support_bottom_interface_spacing", "",29}, {"support_object_xy_distance", "",30}, {"bridge_no_support", "",31},{"max_bridge_length", "",32},{"support_critical_regions_only", "",33},{"support_remove_small_overhang","",34}, - {"build_plate_tilt_x","",35},{"build_plate_tilt_y","",36}, - {"support_object_first_layer_gap","",37} + {"support_object_first_layer_gap","",35} }}, { L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13} }} From 9cca3093ee960ad6772cdb025ac5a9b9a7d2e504 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:19 +0800 Subject: [PATCH 09/75] Keep the Plate Offset When Swapping in the Belt Writer The belt writer replaced the plate-offset-carrying writer mid-export, so belt G-code for any plate but the first kept the plate origin and long-travel clipping used the wrong frame. GCode now remembers the offset and hands it to the new writer, and the writer's first-layer probes use the plate-local point it emits. --- src/libslic3r/BeltGCode.cpp | 1 + src/libslic3r/BeltGCodeWriter.cpp | 7 ++++--- src/libslic3r/GCode.hpp | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 3ba2f79e55..b6108ea78e 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -15,6 +15,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); belt_writer->set_machine_frame_transform(print.config()); + belt_writer->set_xy_offset(m_gcode_offset.x(), m_gcode_offset.y()); m_writer = std::move(belt_writer); } diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index 4824d76f69..a1902f2db4 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -85,7 +85,7 @@ std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string w.emit_xyz(machine); const bool first_layer_for_point = belt_point_on_first_layer( m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(point.x(), point.y(), m_pos.z())); + Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z())); auto speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); @@ -115,7 +115,7 @@ std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment) if (speed == 0.) { const bool first_layer_for_point = belt_point_on_first_layer( m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, - Vec3d(m_pos.x(), m_pos.y(), z)); + Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z)); speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); } @@ -181,7 +181,8 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string Vec3d dest_point = point; const bool first_layer_for_point = belt_point_on_first_layer( - m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point); + m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, + Vec3d(point.x() - m_x_offset, point.y() - m_y_offset, point.z())); auto travel_speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 17a7ce4608..34b0774b34 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -228,7 +228,7 @@ public: void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer - void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + void set_gcode_offset(double x, double y) { m_gcode_offset = Vec2d(x, y); m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } @@ -767,6 +767,8 @@ protected: // printers without a Z-axis shear; in that case all per-path plane // checks short-circuit to the legacy Layer::id() == 0 path. std::unique_ptr m_first_layer_plane; + // Plate origin, kept so a writer replaced during export can be given it again. + Vec2d m_gcode_offset{ Vec2d::Zero() }; std::unique_ptr m_pressure_equalizer; From 509283100709d8653e801a0e95331a50b8ab613b Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 16:28:09 +0800 Subject: [PATCH 10/75] Apply the Belt Slicing Transform to Painted Supports and Seams Painted support/seam facets, support volumes, seam occlusion, MMU and fuzzy skin painting (top/bottom and side facets) and the adaptive infill octree used trafo_centered(), or trafo() with a centre-offset shift, while the layers were sliced with the belt rotation, remap and Z lift; they now share PrintObject::trafo_sliced(). --- src/libslic3r/GCode/SeamPlacer.cpp | 4 ++-- src/libslic3r/MultiMaterialSegmentation.cpp | 11 ++++++----- src/libslic3r/Print.hpp | 3 +++ src/libslic3r/PrintObject.cpp | 10 +++++++--- src/libslic3r/PrintObjectSlice.cpp | 9 ++++++++- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 35bc436d5a..7562c7745d 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -627,7 +627,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po, SeamPosition seam_position = spAligned) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: gather occlusion meshes: start"; - auto obj_transform = po->trafo_centered(); + auto obj_transform = po->trafo_sliced(); indexed_triangle_set triangle_set; indexed_triangle_set negative_volumes_set; //add all parts @@ -712,7 +712,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start"; - auto obj_transform = po->trafo_centered(); + auto obj_transform = po->trafo_sliced(); for (const ModelVolume *mv : po->model_object()->volumes) { if (mv->is_seam_painted()) { diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 6f80f7b759..0016d96ac2 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1215,7 +1215,7 @@ static inline std::vector> segmentation_top_and_bottom_l // project downards pointing painted triangles over bottom surfaces. std::vector> top_raw(num_facets_states), bottom_raw(num_facets_states); std::vector zs = zs_from_layers(layers); - Transform3d object_trafo = print_object.trafo_centered(); + Transform3d object_trafo = print_object.trafo_sliced(); #ifdef MM_SEGMENTATION_DEBUG_TOP_BOTTOM static int iRun = 0; @@ -2039,17 +2039,19 @@ std::vector> segmentation_by_painting(const PrintObject } BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Projection of painted triangles - Begin"; + // The layers were sliced in this frame (belt rotation, remap and Z lift included), and it already centers the object. + const Transform3d object_trafo = print_object.trafo_sliced(); for (const ModelVolume *mv : print_object.model_object()->volumes) { const ModelVolumeFacetsInfo facets_info = extract_facets_info(*mv); - tbb::parallel_for(tbb::blocked_range(1, num_facets_states), [&mv, &print_object, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(1, num_facets_states), [&mv, &object_trafo, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range &range) { for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) { throw_on_cancel_callback(); const indexed_triangle_set custom_facets = facets_info.facets_annotation.get_facets(*mv, EnforcerBlockerType(extruder_idx)); if (!mv->is_model_part() || custom_facets.indices.empty()) continue; - const Transform3f tr = print_object.trafo().cast() * mv->get_matrix().cast(); - tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { + const Transform3f tr = (object_trafo * mv->get_matrix()).cast(); + tbb::parallel_for(tbb::blocked_range(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range &range) { for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) { float min_z = std::numeric_limits::max(); float max_z = std::numeric_limits::lowest(); @@ -2102,7 +2104,6 @@ std::vector> segmentation_by_painting(const PrintObject Line line_to_test(Point(scale_(line_start_f.x()), scale_(line_start_f.y())), Point(scale_(line_end_f.x()), scale_(line_end_f.y()))); - line_to_test.translate(-print_object.center_offset()); // BoundingBoxes for EdgeGrids are computed from printable regions. It is possible that the painted line (line_to_test) could // be outside EdgeGrid's BoundingBox, for example, when the negative volume is used on the painted area (GH #7618). diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index c387dcd552..a801e0688f 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -359,6 +359,9 @@ public: // Trafo with the center_offset() applied after the transformation, to center the object in XY before slicing. Transform3d trafo_centered() const { Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale(m_center_offset.x()), - unscale(m_center_offset.y()), 0)); return t; } + // trafo_centered() with the belt pre-slice transforms applied: the frame the layers were sliced in (Layer::slice_z). + // Equal to trafo_centered() unless a belt rotation or pre-slice remap is active. + Transform3d trafo_sliced() const; const PrintInstances& instances() const { return m_instances; } PrintInstances &instances() { return m_instances; } diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index ac7f34baa7..275998018d 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1101,7 +1101,10 @@ std::pair PrintObject::prepare indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set(); // Rotate mesh and build octree on it with axis-aligned (standart base) cubes. auto to_octree = transform_to_octree().toRotationMatrix(); - its_transform(mesh, to_octree * this->trafo_centered(), true); + // Overhangs below are placed at Layer::bottom_z(), which includes the belt global Z offset. + Transform3d object_trafo = this->trafo_sliced(); + object_trafo.translation().z() += m_belt_global_z_offset; + its_transform(mesh, to_octree * object_trafo, true); // Triangulate internal bridging surfaces. std::vector> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1))); @@ -5124,6 +5127,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index void PrintObject::project_and_append_custom_facets( bool seam, EnforcerBlockerType type, std::vector& out, std::vector>* vertical_points) const { + const Transform3d object_trafo = this->trafo_sliced(); for (const ModelVolume* mv : this->model_object()->volumes) if (mv->is_model_part()) { const indexed_triangle_set custom_facets = seam @@ -5132,12 +5136,12 @@ void PrintObject::project_and_append_custom_facets( if (! custom_facets.indices.empty()) { if (seam) project_triangles_to_slabs(this->layers(), custom_facets, - (this->trafo_centered() * mv->get_matrix()).cast(), + (object_trafo * mv->get_matrix()).cast(), seam, out); else { std::vector projected; // Support blockers or enforcers. Project downward facing painted areas upwards to their respective slicing plane. - slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, vertical_points, [](){}); + slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), object_trafo * mv->get_matrix(), nullptr, &projected, vertical_points, [](){}); // Merge these projections with the output, layer by layer. assert(! projected.empty()); assert(out.empty() || out.size() == projected.size()); diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 2b6c4ffcff..8360a9562f 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1806,6 +1806,13 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_ return union_ex(new_ex_polys); } +Transform3d PrintObject::trafo_sliced() const +{ + Transform3d trafo = this->trafo_centered(); + BeltSliceStrategy::apply_preslice_transforms(trafo, this->print()->config(), this->model_object()->volumes); + return trafo; +} + std::vector PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const { auto it_volume = this->model_object()->volumes.begin(); @@ -1820,7 +1827,7 @@ std::vector PrintObject::slice_support_volumes(const ModelVolumeType m const Print *print = this->print(); auto throw_on_cancel_callback = std::function([print](){ print->throw_if_canceled(); }); MeshSlicingParamsEx params; - params.trafo = this->trafo_centered(); + params.trafo = this->trafo_sliced(); for (; it_volume != it_volume_end; ++ it_volume) if ((*it_volume)->type() == model_volume_type) { std::vector slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback); From 6e33f3f5dd11dee5250188b6acc540442fb1295c Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:58:45 +0800 Subject: [PATCH 11/75] Share and Clamp the Build Plate Tilt Shift in Support Generators The three support generators each computed lh * tan(tilt), which overflows coord_t at 90 degrees and flips sign beyond it (belt sync can write up to 180). One helper now returns the tilt slope with the tilt capped at 89 degrees. --- src/libslic3r/Support/SupportCommon.cpp | 6 ++++++ src/libslic3r/Support/SupportCommon.hpp | 4 ++++ src/libslic3r/Support/SupportMaterial.cpp | 10 +++------- src/libslic3r/Support/TreeSupport.cpp | 10 +++------- src/libslic3r/Support/TreeSupport3D.cpp | 12 ++++-------- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 27ac9b24d1..0c3d570b1d 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -2069,4 +2069,10 @@ sub clip_with_shape { } */ +Vec2d build_plate_tilt_slope(const PrintConfig &print_config) +{ + auto slope = [](double tilt_deg) { return std::tan(Geometry::deg2rad(std::clamp(tilt_deg, -89., 89.))); }; + return { slope(print_config.build_plate_tilt_y.value), slope(print_config.build_plate_tilt_x.value) }; +} + } // namespace Slic3r diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index f15c7d417a..4374986a3e 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -150,6 +150,10 @@ Polygons belt_floor_surface_polygon( const SlicingParameters &slicing_params, const PrintConfig &print_config, const PrintObject &object, coordf_t print_z); +// Build plate tilt: XY drift of gravity per unit of layer height, zero on a level plate. +// The tilt is capped below 90 degrees to keep the drift finite. +Vec2d build_plate_tilt_slope(const PrintConfig &print_config); + } // namespace Slic3r #endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index c8ac217af2..5d886bcdf0 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1438,9 +1438,8 @@ static inline ExPolygons detect_overhangs( const bool bridge_no_support = object_config.bridge_no_support.value; const coordf_t xy_expansion = scale_(object_config.support_expansion.value); // Build plate tilt: compute per-layer XY shift for tilted gravity direction - const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + const Vec2d tilt_slope = build_plate_tilt_slope(print_config); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; float lower_layer_offset = 0; if (layer_id == 0) @@ -1480,10 +1479,7 @@ static inline ExPolygons detect_overhangs( Polygons tilted_lower; if (has_tilt) { tilted_lower = lower_layer_polygons; - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(tilted_lower, tilt_shift); + translate(tilted_lower, Point::new_scale(tilt_slope * lower_layer.height)); effective_lower = &tilted_lower; } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 84ae229130..a86aea3536 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -709,9 +709,8 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) const double threshold_rad = Geometry::deg2rad(thresh_angle); // Build plate tilt: compute per-layer XY shift for tilted gravity direction const PrintConfig& print_cfg = m_object->print()->config(); - const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + const Vec2d tilt_slope = build_plate_tilt_slope(print_cfg); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -858,10 +857,7 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons shifted_lower; if (has_tilt) { shifted_lower = lower_polys; // copy - const double lh = lower_layer->height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(shifted_lower, tilt_shift); + translate(shifted_lower, Point::new_scale(tilt_slope * lower_layer->height)); } const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index e02f474420..b5c35bfd6b 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -211,9 +211,8 @@ static std::vector>> group_me // +1 makes the threshold inclusive double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.); // Build plate tilt: compute per-layer XY shift for tilted gravity direction - const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value); - const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value); - const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + const Vec2d tilt_slope = build_plate_tilt_slope(print_config); + const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON; //FIXME this is a fudge constant! auto enforcer_overhang_offset = scaled(config.tree_support_tip_diameter.value); const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution; @@ -235,7 +234,7 @@ static std::vector>> group_me size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size())); tbb::parallel_for(tbb::blocked_range(1, num_overhang_layers), [&print_object, &config, &print_config, &enforcers_layers, &blockers_layers, - support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out] + support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_slope, &throw_on_cancel, &out] (const tbb::blocked_range &range) { for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { const Layer ¤t_layer = *print_object.get_layer(layer_id); @@ -263,10 +262,7 @@ static std::vector>> group_me Polygons lower_layer_offseted; if (has_tilt) { Polygons lower_src = to_polygons(lower_layer.lslices_extrudable); - const double lh = lower_layer.height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); - translate(lower_src, tilt_shift); + translate(lower_src, Point::new_scale(tilt_slope * lower_layer.height)); lower_layer_offseted = offset(lower_src, lower_layer_offset); } else { lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset); From 4ed56954e8be7e1d9e5fb355628816454d51985b Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:05 +0800 Subject: [PATCH 12/75] Limit Build Plate Tilt Range Below 90 Degrees A 90 degree tilt has no finite gravity drift per layer, so the option range now stops at 89 degrees, matching the cap applied by the support generators. --- src/libslic3r/PrintConfig.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ceeb60af92..a6a419a15e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7163,8 +7163,8 @@ void PrintConfigDef::init_fff_params() "A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. " "In belt printer mode, this is automatically synced to the belt angle."); def->sidetext = u8"\u00B0"; - def->min = -90; - def->max = 90; + def->min = -89; + def->max = 89; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); @@ -7175,8 +7175,8 @@ void PrintConfigDef::init_fff_params() "A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. " "A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt."); def->sidetext = u8"\u00B0"; - def->min = -90; - def->max = 90; + def->min = -89; + def->max = 89; def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(0.)); From f2a11928f63f371c92ad8f4a33ae35379e06d3bb Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 20:25:08 +0800 Subject: [PATCH 13/75] Read the Belt Tilt Only from the Belt G-code Header Every printer's config block lists belt_slice_rotation_angle (default 45), so the processor marked all G-code as belt G-code: imported flat G-code got the belt view on a belt printer, and the belt-only Z handling in the processor ran for non-belt prints whose config block precedes the body. Take the angle only from outside the config block, where only the belt header writes it. --- src/libslic3r/GCode/GCodeProcessor.cpp | 16 +++++++-- src/libslic3r/GCode/GCodeProcessor.hpp | 1 + tests/fff_print/CMakeLists.txt | 1 + tests/fff_print/test_gcode_processor.cpp | 46 ++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 tests/fff_print/test_gcode_processor.cpp diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index b2477437b0..33d5b96876 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3586,6 +3586,7 @@ void GCodeProcessor::reset() m_zero_layer_height = 0.0f; m_first_layer_height = 0.0f; m_processing_start_custom_gcode = false; + m_in_config_block = false; m_g1_line_id = 0; m_layer_id = 0; m_cp_color.reset(); @@ -4191,9 +4192,20 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return; } + if (boost::starts_with(comment, " CONFIG_BLOCK_START")) { + m_in_config_block = true; + return; + } + if (boost::starts_with(comment, " CONFIG_BLOCK_END")) { + m_in_config_block = false; + return; + } + // Belt printer: derive the physical tilt magnitude from the slicing-rotation - // angle header comment (used to enable the preview's belt view). - if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) { + // angle header comment (used to enable the preview's belt view). Only the belt + // header carries it outside the config block; the config block lists the key + // for every printer, belt or not. + if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) { try { m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29)))); } catch (...) {} diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 0a8a35a88b..85cc5a7544 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1194,6 +1194,7 @@ class Print; float m_first_layer_height; // mm float m_zero_layer_height; // mm bool m_processing_start_custom_gcode; + bool m_in_config_block; unsigned int m_g1_line_id; unsigned int m_layer_id; CpColor m_cp_color; diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index 3247bfda66..59e065ecb8 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests test_extrusion_processor.cpp test_fill.cpp test_flow.cpp + test_gcode_processor.cpp test_gcode_timing.cpp test_gcodewriter.cpp test_model.cpp diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp new file mode 100644 index 0000000000..3998b65547 --- /dev/null +++ b/tests/fff_print/test_gcode_processor.cpp @@ -0,0 +1,46 @@ +#include + +#include "libslic3r/libslic3r.h" +#include "libslic3r/GCode/GCodeProcessor.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include "test_utils.hpp" + +#include +#include + +using namespace Slic3r; +using Catch::Matchers::WithinAbs; + +namespace { + +float processed_belt_tilt(const std::string &gcode) +{ + ScopedTemporaryFile temp(".gcode"); + { + std::ofstream os(temp.string()); + os << gcode; + } + GCodeProcessor proc; + proc.apply_config(FullPrintConfig{}); + proc.process_file(temp.string()); + return proc.get_result().belt_tilt_angle; +} + +constexpr const char *body = "G1 X10 Y10 Z0.2 F3000\nG1 X20 Y10 E1 F1200\n"; + +} // namespace + +TEST_CASE("The config block's belt angle does not mark G-code as belt G-code", "[GCodeProcessor][belt]") +{ + // Every printer's config block lists belt_slice_rotation_angle (default 45), belt or not. + const std::string gcode = std::string("; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n") + body; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(0., 1e-6)); +} + +TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcessor][belt]") +{ + const std::string gcode = std::string("; belt_slice_rotation_angle = -45.0\n") + body + + "; CONFIG_BLOCK_START\n; belt_printer = 1\n; belt_slice_rotation_angle = -45\n; CONFIG_BLOCK_END\n"; + CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(45., 1e-6)); +} From 4a7311bf0160e54aa9d912a1d92f06beda8ccb61 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:57:58 +0800 Subject: [PATCH 14/75] Rebuild the Brim Type Combobox Only When Its Entries Change toggle_options() now runs on every value change and mode switch; rebuild the brim_type choices only when the leading-edge entry has to be added or removed. --- src/slic3r/GUI/Tab.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1c03ddf88a..2c1247edb5 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3230,16 +3230,17 @@ void TabPrint::toggle_options() const auto current = m_config->opt_enum("brim_type"); auto &opt = const_cast(field->m_opt); auto cb = dynamic_cast(choice->window); - if (cb != nullptr) { + // Keep the entry if it is already selected, so switching to a non-belt + // printer cannot leave the control showing a value it does not offer. + const bool offer_leading_edge = is_belt_printer || current == btLeadingEdgeOnly; + const bool offered = std::find(opt.enum_values.begin(), opt.enum_values.end(), "leading_edge_only") != opt.enum_values.end(); + if (cb != nullptr && offer_leading_edge != offered) { auto n = cb->GetValue(); opt.enum_values.clear(); opt.enum_labels.clear(); cb->Clear(); for (size_t i = 0; i < def->enum_values.size(); ++ i) { - // Keep the entry if it is already selected, so switching to a non-belt - // printer cannot leave the control showing a value it does not offer. - if (def->enum_values[i] == "leading_edge_only" && ! is_belt_printer - && current != btLeadingEdgeOnly) + if (def->enum_values[i] == "leading_edge_only" && ! offer_leading_edge) continue; opt.enum_values.push_back(def->enum_values[i]); opt.enum_labels.push_back(def->enum_labels[i]); From 3b302b466617e336b6723e294317b06c138b62a6 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:04:54 +0800 Subject: [PATCH 15/75] Derive Belt Support Tilt From Slicing Rotation in Print::apply build_plate_tilt_x/y was synced from belt_slice_rotation* only by the printer Tab, so CLI or 3MF edits of the rotation left the support tilt stale. --- src/libslic3r/PrintApply.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index aa5c6aa2bd..0ed85f7fbc 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1241,6 +1241,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", i=%1%, key=%2%")%i %changed_keys[i]; } } + // On belt printers the support tilt follows the slicing rotation. The GUI keeps the two in + // sync, but a CLI or 3MF edit of the rotation alone would otherwise leave supports on a stale tilt. + if (const auto *belt_opt = new_full_config.option("belt_printer"); belt_opt && belt_opt->value) { + const auto *axis_opt = new_full_config.option>("belt_slice_rotation"); + const auto *angle_opt = new_full_config.option("belt_slice_rotation_angle"); + if (axis_opt && angle_opt) { + const auto tilt = BeltTransformPipeline::physical_tilt(axis_opt->value, angle_opt->value); + new_full_config.set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg)); + new_full_config.set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg)); + } + } const ConfigOption* enable_support_option = new_full_config.option("enable_support"); if (enable_support_option && enable_support_option->getBool()) m_support_used = true; From 708212a306f6f39dd051be81249340572e301047 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 03:02:21 +0800 Subject: [PATCH 16/75] Share One Belt Brim Band Loop Between Apron-Only and Ordinary Layers The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged. --- src/libslic3r/GCode.cpp | 40 ++++++++-------------------------------- src/libslic3r/GCode.hpp | 10 ++++++---- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 284c815f2d..4c742ed8df 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5388,7 +5388,8 @@ LayerResult GCode::process_belt_brim_layer( // object layer, takes the ordinary path, and the band would be silently dropped. std::string GCode::emit_belt_brim_bands(const Print &print, const std::vector &layers, - const size_t single_object_instance_idx) + const size_t single_object_instance_idx, + const std::optional extruder_id) { std::string gcode; for (const LayerToPrint <p : layers) { @@ -5396,6 +5397,9 @@ std::string GCode::emit_belt_brim_bands(const Print &print, if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) continue; const PrintObject &object = *ltp.original_object; + // No filter prints every band; belt_brim_filament() is 1-based. + if (extruder_id && (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != *extruder_id)) + continue; // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); m_config.apply(object.config(), true); @@ -6623,39 +6627,11 @@ LayerResult GCode::process_layer( std::set> belt_brim_emitted; // Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an - // object/support layer) whose brim filament is this pass's extruder. Mirrors - // emit_belt_brim_bands() per band, but filtered to one brim filament so each band + // object/support layer) whose brim filament is this pass's extruder, so each band // prints in the correct tool's pass (Finding B). extruder_id is 0-based (the - // reindexed tool domain); belt_brim_filament() is 1-based, so subtract one. + // reindexed tool domain). auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string { - std::string gc; - for (const LayerToPrint <p : layers) { - const BeltBrimBand *band = ltp.belt_brim_band; - if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) - continue; - const PrintObject &object = *ltp.original_object; - if (! object.has_belt_brim() || (unsigned int)(object.belt_brim_filament() - 1) != extruder_id) - continue; - // Speeds, flow and retraction all read m_config. - m_config.apply(print.default_region_config()); - m_config.apply(object.config(), true); - const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx; - const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size() - : single_object_instance_idx + 1; - for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) { - // Band geometry is object-local, like the object's own extrusions. - const Point &offset = object.instances()[i].shift; - this->set_origin(unscale(offset)); - this->on_set_origin(&object, offset); - m_avoid_crossing_perimeters.use_external_mp(); - for (const ExtrusionEntity *ee : band->fills.entities) - if (ee != nullptr) - gc += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed)); - m_avoid_crossing_perimeters.use_external_mp(false); - m_avoid_crossing_perimeters.disable_once(); - } - } - return gc; + return this->emit_belt_brim_bands(print, layers, single_object_instance_idx, extruder_id); }; for (unsigned int extruder_id : layer_tools.extruders) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 34b0774b34..23fb563711 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -418,13 +418,15 @@ protected: const bool last_layer, const size_t single_object_instance_idx); - // Emit the apron bands carried by these layers. Called from both the brim-only - // branch and the ordinary path, since a band's print_z can coincide with another - // object's layer on a multi-object belt. + // Emit the apron bands carried by these layers, only those whose brim filament is + // extruder_id (0-based) when given. Called from both the brim-only branch and the + // ordinary path, since a band's print_z can coincide with another object's layer on a + // multi-object belt. std::string emit_belt_brim_bands( const Print &print, const std::vector &layers, - const size_t single_object_instance_idx); + const size_t single_object_instance_idx, + const std::optional extruder_id = std::nullopt); LayerResult process_layer( const Print &print, From 19a085206ca5e7594d295c153e4afb9e113b6aa1 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 03:02:36 +0800 Subject: [PATCH 17/75] Print Belt Brim Aprons in Each Object's Brim Filament Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange. --- src/libslic3r/GCode.cpp | 13 +++++++++---- src/libslic3r/GCode.hpp | 9 ++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4c742ed8df..bc117f5d1b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5374,7 +5374,12 @@ LayerResult GCode::process_belt_brim_layer( print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n"; } - gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx); + // Objects sharing this apron Z may use different brim filaments; print each in its own tool. + for (const unsigned int brim_extruder : layer_tools.extruders) { + if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim_extruder) + gcode += this->set_extruder(brim_extruder, print_z); + gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx, brim_extruder); + } result.gcode = std::move(gcode); return result; @@ -5389,7 +5394,7 @@ LayerResult GCode::process_belt_brim_layer( std::string GCode::emit_belt_brim_bands(const Print &print, const std::vector &layers, const size_t single_object_instance_idx, - const std::optional extruder_id) + const unsigned int extruder_id) { std::string gcode; for (const LayerToPrint <p : layers) { @@ -5397,8 +5402,8 @@ std::string GCode::emit_belt_brim_bands(const Print &print, if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr) continue; const PrintObject &object = *ltp.original_object; - // No filter prints every band; belt_brim_filament() is 1-based. - if (extruder_id && (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != *extruder_id)) + // belt_brim_filament() is 1-based. + if (! object.has_belt_brim() || static_cast(object.belt_brim_filament() - 1) != extruder_id) continue; // Speeds, flow and retraction all read m_config. m_config.apply(print.default_region_config()); diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 23fb563711..afa7151a5c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -418,15 +418,14 @@ protected: const bool last_layer, const size_t single_object_instance_idx); - // Emit the apron bands carried by these layers, only those whose brim filament is - // extruder_id (0-based) when given. Called from both the brim-only branch and the - // ordinary path, since a band's print_z can coincide with another object's layer on a - // multi-object belt. + // Emit the apron bands carried by these layers whose brim filament is extruder_id + // (0-based). Called from both the brim-only branch and the ordinary path, since a + // band's print_z can coincide with another object's layer on a multi-object belt. std::string emit_belt_brim_bands( const Print &print, const std::vector &layers, const size_t single_object_instance_idx, - const std::optional extruder_id = std::nullopt); + const unsigned int extruder_id); LayerResult process_layer( const Print &print, From cb5b489e9010c3d0a978749714786d3336a159a3 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:57:27 +0800 Subject: [PATCH 18/75] Invalidate Only G-code Export for Belt Output Options gcode_back_transform, first_layer_plane* and belt_printer_infinite_y fell through to invalidate_all_steps(), which re-ran tool ordering, skirt/brim and G-code export on toggles that only affect G-code export. --- src/libslic3r/Print.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index ba838003ea..b97ffed3a2 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -121,6 +121,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "gcode_remap_z", // Machine-frame transform (derived from belt tilt; only affects G-code output). "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + "gcode_back_transform", + "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", + // Only inflates the GUI bed volume, like printable_area. + "belt_printer_infinite_y", //BBS "additional_cooling_fan_speed", "reduce_crossing_wall", From 76aba24ddfeea6249f8902061db1bea3e66f5b12 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:34 +0800 Subject: [PATCH 19/75] Number Belt Extension Support Layers Sequentially Extension layers were all created with id 0, so every one of them could be taken for the first layer by id-only checks (ooze-prevention standby temperature, cached layer ids). Renumber the support layers after inserting them. --- src/libslic3r/Support/TreeSupport.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index a86aea3536..1f39a56163 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1913,6 +1913,8 @@ 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()); + for (size_t i = 0; i < sl_vec.size(); ++i) + sl_vec[i]->set_id(i); } } } From 33aec258da17c4988d35ada0445a13d48a9531eb Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 14 Sep 2026 16:42:33 +0800 Subject: [PATCH 20/75] Skip CLI Wipe Tower Reservation on Belt Printers Print::has_wipe_tower() is always false for belt printers, but CLI arrange, plate checks and the pre-slice tower clamp still reserved a phantom tower footprint and wrote a clamped wipe_tower_x/y into the config. --- src/OrcaSlicer.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index b75c653eda..15c5677485 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -4014,6 +4014,10 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__; } + // Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it. + const ConfigOptionBool* belt_printer_opt = m_print_config.option("belt_printer"); + const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value; + auto timelapse_type_opt = m_print_config.option("timelapse_type"); bool is_smooth_timelapse = false; if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth)) @@ -4251,11 +4255,11 @@ int CLI::run(int argc, char **argv) } }; - auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) { + auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) { plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box(); BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}") %(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z(); - if (!print_config.has("wipe_tower_x")) { + if (is_belt_printer || !print_config.has("wipe_tower_x")) { plate_obj_size_info.has_wipe_tower = false; BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower"); return; @@ -5062,7 +5066,7 @@ int CLI::run(int argc, char **argv) } } - if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty())) + if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty()))) { //prepare the wipe tower int plate_count = partplate_list.get_plate_count(); @@ -5212,7 +5216,7 @@ int CLI::run(int argc, char **argv) bool is_seq_print = false; get_print_sequence(cur_plate, m_print_config, is_seq_print); - if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position) + if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position) { //prepare the wipe tower auto printer_structure_opt = m_print_config.option>("printer_structure"); @@ -5361,7 +5365,7 @@ int CLI::run(int argc, char **argv) //add the virtual object into unselect list if has partplate_list.preprocess_exclude_areas(unselected, enable_wrapping_detect); - if (used_filament_set.size() > 0) + if (!is_belt_printer && used_filament_set.size() > 0) { //prepare the wipe tower int plate_count = partplate_list.get_plate_count(); @@ -5467,7 +5471,7 @@ int CLI::run(int argc, char **argv) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode"); } - if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) { + if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) { float x; float y; if (duplicate_count > 0) { @@ -6037,7 +6041,7 @@ int CLI::run(int argc, char **argv) // The stored (or default) tower position may not fit the tower these plates // need, and no CLI placement site runs on a plain slice - mirror the GUI's // reload clamp and fit every plate's tower into the printable area first. - if (m_print_config.option("enable_prime_tower", true)->value) { + if (!is_belt_printer && m_print_config.option("enable_prime_tower", true)->value) { for (int index = 0; index < partplate_list.get_plate_count(); index++) { if ((plate_to_slice != 0) && (plate_to_slice != (index + 1))) continue; From 5cd66612801e6c851bc09352af05ee9acbc7faec Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 15:59:38 +0800 Subject: [PATCH 21/75] Check the Belt Temperature Tower Model Load Bail out like the other calibration paths when add_model() fails instead of indexing the empty model. --- src/slic3r/GUI/Plater.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 25084e9a9c..b6274ab7a2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -16556,7 +16556,8 @@ void Plater::calib_temp(const Calib_Params& params) { << ", falling back to 230_190 (embossed numbers will not match)"; asset = calib_dir + "belt_temp_tower_230_190.stl"; } - add_model(false, asset); + if (!add_model(false, asset) || model().objects.empty()) + return; // Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode // starts at 0, centered laterally on the bed, resting on the conveyor. From a8a45439db350cf1f026e70dd017a8b39996f570 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:05:40 +0800 Subject: [PATCH 22/75] Share One Build Plate Tilt Up-Direction Helper Across the GUI The bed gravity arrow, volume rendering and the painter/support gizmos each rebuilt the tilt up-vector from build_plate_tilt_x/y; use one helper that also tolerates presets without the keys. --- src/slic3r/GUI/3DBed.cpp | 12 +++--------- src/slic3r/GUI/3DScene.cpp | 12 +----------- src/slic3r/GUI/GUI_App.cpp | 13 +++++++++++++ src/slic3r/GUI/GUI_App.hpp | 2 ++ src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 20 +++----------------- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp | 1 - src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 9 +-------- 7 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index fa9acffb50..3032059678 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -746,20 +746,14 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; // build_plate_tilt_{x,y} are kept in sync with the belt tilt (see TabPrinter), so // reading them here covers both belt and non-belt tilted printers. - double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg == 0. && tilt_y_deg == 0.) { + const Vec3d up_dir = build_plate_tilt_up_direction(); + if (up_dir == Vec3d::UnitZ()) { m_gravity_arrow.reset(); return; } - - // Gravity direction (matching the slicer's tilt convention) - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const Vec3d gravity_dir = -up_dir; // Build the arrow model (same dimensions as the axis arrows) if (!m_gravity_arrow.is_initialized()) { diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 81b0ee8028..b5c5c4fa16 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -1155,17 +1155,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type, // Compute up direction accounting for build plate tilt. This is frame-invariant // (config cannot change mid-render), so compute it once before the volume loop. - Vec3f up_direction = Vec3f::UnitZ(); - { - const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg != 0. || tilt_y_deg != 0.) { - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); - } - } + const Vec3f up_direction = GUI::build_plate_tilt_up_direction().cast(); for (GLVolumeWithIdAndZ& volume : to_render) { #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index 966cf49013..512ddd315e 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -81,6 +81,7 @@ #include #include "libslic3r/Utils.hpp" +#include "libslic3r/Geometry.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/I18N.hpp" #include "libslic3r/PresetBundle.hpp" @@ -9812,5 +9813,17 @@ bool is_support_filament(int extruder_id, bool strict_check) return support_option->get_at(0); }; +Vec3d build_plate_tilt_up_direction() +{ + const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *opt_x = cfg.option("build_plate_tilt_x"); + const auto *opt_y = cfg.option("build_plate_tilt_y"); + const double tilt_x = opt_x != nullptr ? opt_x->value : 0.; + const double tilt_y = opt_y != nullptr ? opt_y->value : 0.; + if (tilt_x == 0. && tilt_y == 0.) + return Vec3d::UnitZ(); + return Vec3d(std::tan(Geometry::deg2rad(tilt_y)), std::tan(Geometry::deg2rad(tilt_x)), 1.).normalized(); +} + } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index f6f0b81c92..1355d7728d 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -831,6 +831,8 @@ bool is_support_filament(int extruder_id, bool strict_check = true); bool is_soluble_filament(int extruder_id); // check if the filament for model is in the list bool has_filaments(const std::vector& model_filaments); +// Up direction of the edited printer's tilted build plate (+Z when untilted). +Vec3d build_plate_tilt_up_direction(); } // namespace GUI } // Slic3r diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 468e2e9b6c..78a3d4a834 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -548,14 +548,6 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle() return auto_support ? support_threshold_angle : 0; } -std::pair GLGizmoFdmSupports::get_build_plate_tilt() -{ - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x = cfg.opt_float("build_plate_tilt_x"); - double tilt_y = cfg.opt_float("build_plate_tilt_y"); - return {tilt_x, tilt_y}; -} - void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) { float threshold = (float(M_PI)/180.f)*threshold_deg; @@ -564,15 +556,9 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block) const ModelInstance* mi = mo->instances[selection.get_instance_idx()]; // Compute gravity direction accounting for build plate tilt - auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt(); - double tilt_x_rad = tilt_x_deg * M_PI / 180.0; - double tilt_y_rad = tilt_y_deg * M_PI / 180.0; - const bool has_tilt = (tilt_x_deg != 0. || tilt_y_deg != 0.); - // NB: use an if, not a ?:, so each branch converts to Vec3d independently - // (the two Eigen expression types don't unify in a ternary). - Vec3d gravity_dir = -Vec3d::UnitZ(); - if (has_tilt) - gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized(); + const Vec3d up_dir = build_plate_tilt_up_direction(); + const bool has_tilt = up_dir != Vec3d::UnitZ(); + const Vec3d gravity_dir = -up_dir; int mesh_id = -1; for (const ModelVolume* mv : mo->volumes) { diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp index b313ca04aa..57a6cc1981 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.hpp @@ -60,7 +60,6 @@ private: void select_facets_by_angle(float threshold, bool block); // BBS int get_selection_support_threshold_angle(); - std::pair get_build_plate_tilt(); int m_support_threshold_angle = -1; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index ac0f46df34..ad9f69eb9b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -75,14 +75,7 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl Vec3f GLGizmoPainterBase::get_tilt_up_direction() const { - const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config; - double tilt_x_deg = cfg.opt_float("build_plate_tilt_x"); - double tilt_y_deg = cfg.opt_float("build_plate_tilt_y"); - if (tilt_x_deg == 0. && tilt_y_deg == 0.) - return Vec3f::UnitZ(); - double tilt_x_rad = Geometry::deg2rad(tilt_x_deg); - double tilt_y_rad = Geometry::deg2rad(tilt_y_deg); - return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized(); + return build_plate_tilt_up_direction().cast(); } void GLGizmoPainterBase::render_triangles(const Selection& selection) const From 62ef0fa4f8f2d6af9ef9c88cb3baf57724f0ae00 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Sun, 13 Sep 2026 03:03:00 +0800 Subject: [PATCH 23/75] Write the Standard Layer Change Tag on Belt Brim Apron Layers Apron layers appended print_z to the layer change tag, unlike every other layer; write the plain tag line. --- src/libslic3r/GCode.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index bc117f5d1b..0640860ec8 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5342,8 +5342,7 @@ LayerResult GCode::process_belt_brim_layer( // skip these layers entirely. { char buf[64]; - sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z); - gcode += buf; + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n"; sprintf(buf, ";Z:%g\n", print_z); gcode += buf; const float band_height = float(height); From 18c08862a80b0e4bc2550addca69aa975e88d1d6 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:04:12 +0800 Subject: [PATCH 24/75] Align up_direction Position in TriangleSelector Fill Calls seed_fill_select_triangles() now takes up_direction right after highlight_by_angle_deg, as select_patch() does. --- src/libslic3r/TriangleSelector.cpp | 2 +- src/libslic3r/TriangleSelector.hpp | 4 ++-- src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 7e911cc565..840d3c672b 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -367,7 +367,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp) void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate, const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg, - bool force_reselection, const Vec3f &up_direction) + const Vec3f &up_direction, bool force_reselection) { assert(facet_start < m_orig_size_indices); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 6154f7d7f3..e715d976ab 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -336,8 +336,8 @@ public: const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only float seed_fill_angle, // the maximal angle between two facets to be painted by the same color float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees. - bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle - const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt) + const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt) + bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle void bucket_fill_select_triangles(const Vec3f &hit, // point where to start int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp index ad9f69eb9b..d85c635b5a 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPainterBase.cpp @@ -700,7 +700,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() : mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix(); m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); m_triangle_selectors[m_rr.mesh_id]->request_update_render_data(); m_seed_fill_last_mesh_id = m_rr.mesh_id; } @@ -864,7 +864,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true); @@ -962,7 +962,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix); if (m_tool_type == ToolType::SMART_FILL) m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle, - m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction()); + m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), false); else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER) // BBS: add infill_angle parameter m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false); From 14cf7861e063ae9c3d41b0cb7c4444517c525865 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:01:26 +0800 Subject: [PATCH 25/75] Fix Belt Tooltip Spacing and Legend Casing Drop double spaces in the belt tilt tooltips and match the preview legend header to the "Belt printer" settings group. --- src/slic3r/GUI/GCodeViewer.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index ae504d3746..16e7262470 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -4923,7 +4923,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv ImGui::Spacing(); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt Printer").c_str()); + ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt printer").c_str()); ImGui::Dummy({ window_padding, 0 }); ImGui::SameLine(); // Checked = show the raw machine-frame G-code (designed/upright view off). Worded to diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 2c1247edb5..9905a203ba 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5105,7 +5105,7 @@ void TabPrinter::build_fff() { Line line = { L("Belt tilt"), L("Belt tilt axis and angle, applied as a mesh rotation before " - "slicing. Also drives bed rendering and support gravity tilt. " + "slicing. Also drives bed rendering and support gravity tilt. " "Isometric (no distortion); the back-transform inverts it before " "the machine-frame remap.") }; line.append_option(belt_og->get_option("belt_slice_rotation")); @@ -5160,7 +5160,7 @@ void TabPrinter::build_fff() { Line line = { L("Machine-frame tilt"), L("The machine-frame shear (tan) and scale (1/cos) are derived from " - "the belt tilt angle. Enable 'Decouple' to set an independent " + "the belt tilt angle. Enable 'Decouple' to set an independent " "machine-frame angle when the physical gantry tilt differs from " "the slicing rotation.") }; line.append_option(mf->get_option("belt_frame_tilt_decouple")); From b4052ec99f796047871fae17e799d69ffc1091c0 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:01:48 +0800 Subject: [PATCH 26/75] Drop the Redundant Lift Type Alias in eager_lift effective_type was a plain copy of the parameter. --- src/libslic3r/GCodeWriter.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index 67dd6cf138..a2d0a26b6e 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -836,7 +836,6 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase) // BBS: immediately execute an undelayed lift move with a spiral lift pattern // designed specifically for subsequent gcode injection (e.g. timelapse) std::string GCodeWriter::eager_lift(const LiftType type) { - const LiftType effective_type = type; std::string lift_move; double target_lift = 0; { @@ -850,7 +849,7 @@ std::string GCodeWriter::eager_lift(const LiftType type) { } // BBS: spiral lift only safe with known position - if (effective_type == LiftType::SpiralLift && this->is_current_position_clear()) { + if (type == LiftType::SpiralLift && this->is_current_position_clear()) { double radius = target_lift / (2 * PI * atan(filament()->travel_slope())); // static spiral alignment when no move in x,y plane. // spiral centra is a radius distance to the right (y=0) From 2b1a7e38dfd6bab9ad76295b37339a1a926a55cb Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:03:14 +0800 Subject: [PATCH 27/75] Drop Redundant Belt Checks in BeltGCode BeltGCode is only created for belt printers, so its hooks no longer re-check belt_printer, and the BBL-machine flag is set once on whichever writer survives init_belt_writer instead of on one about to be discarded. --- src/libslic3r/BeltGCode.cpp | 11 ++--------- src/libslic3r/BeltGCode.hpp | 2 +- src/libslic3r/GCode.cpp | 5 ++--- src/libslic3r/GCode.hpp | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index b6108ea78e..1b2d76ff76 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -5,13 +5,9 @@ namespace Slic3r { -void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) +void BeltGCode::init_belt_writer(Print &print) { - if (!print.config().belt_printer.value) - return; - auto belt_writer = std::make_unique(); - belt_writer->set_is_bbl_machine(is_bbl_printers); // Axis remap and build volume max are set by base GCode after init_belt_writer returns. belt_writer->set_belt_back_transform(print.config()); belt_writer->set_machine_frame_transform(print.config()); @@ -21,9 +17,6 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers) void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) { - if (!print.config().belt_printer.value) - return; - const auto &full_cfg = print.full_print_config(); // Slicing rotation: the belt tilt (axis + angle) and the single source of truth // for the physical tilt the G-code viewer uses to enable belt view. @@ -62,7 +55,7 @@ void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_ || (m_config.belt_slice_rotation_global.value && m_config.belt_slice_rotation.value != BeltRotationAxis::None && std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON); - if (!use_global || !m_config.belt_printer.value) + if (!use_global) return; // Adjust origin: transform through belt forward pipeline so that diff --git a/src/libslic3r/BeltGCode.hpp b/src/libslic3r/BeltGCode.hpp index 2a3718b49f..b68bb1066f 100644 --- a/src/libslic3r/BeltGCode.hpp +++ b/src/libslic3r/BeltGCode.hpp @@ -14,7 +14,7 @@ namespace Slic3r { class BeltGCode : public GCode { protected: - void init_belt_writer(Print &print, bool is_bbl_printers) override; + void init_belt_writer(Print &print) override; void write_belt_header(GCodeOutputStream &file, const Print &print) override; void on_set_origin(const PrintObject *obj, const Point &inst_shift) override; bool should_disable_arc_fitting() const override { return true; } diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 0640860ec8..05ec6ebc78 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2961,10 +2961,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato m_fan_mover.release(); m_ordering_cache.clear(); - m_writer->set_is_bbl_machine(is_bbl_printers); - // Belt printer: initialize belt-specific writer via virtual hook. - this->init_belt_writer(print, is_bbl_printers); + this->init_belt_writer(print); + m_writer->set_is_bbl_machine(is_bbl_printers); // Standalone axis remap (works with or without belt mode). // Sync the writer's remap state to the current export UNCONDITIONALLY — even at diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index afa7151a5c..18cb7868a2 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -376,7 +376,7 @@ protected: // Virtual hooks for belt printer subclass (BeltGCode). // No-ops in base GCode; overridden in BeltGCode. - virtual void init_belt_writer(Print &print, bool is_bbl_printers) {} + virtual void init_belt_writer(Print &print) {} virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {} virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {} virtual bool should_disable_arc_fitting() const { return false; } From 2b2c710626330fca12570bf32bcf538b0f521c4c Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 11 Sep 2026 16:03:41 +0800 Subject: [PATCH 28/75] Order GCodeWriter Initializers Like the Members Moved to Protected The lift, speed and cached-extruder members now live in the protected section ahead of the private ones; list their initializers first so the list reads in construction order. No behaviour change. --- src/libslic3r/GCodeWriter.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 43e869910e..c73ccc3e07 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -18,17 +18,18 @@ public: bool multiple_extruders; GCodeWriter() : - multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr), - m_curr_extruder_id (-1), - m_cached_extruder_idx(0), - m_single_extruder_multi_material(false), - m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0), - m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0), - m_last_bed_temperature(0), m_last_bed_temperature_reached(true), + multiple_extruders(false), m_lifted(0), m_to_lift(0), m_to_lift_type(LiftType::NormalLift), - m_current_speed(3600), m_is_first_layer(true) + m_is_first_layer(true), m_current_speed(3600), + m_cached_extruder_idx(0), + m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr), + m_curr_extruder_id (-1), + m_single_extruder_multi_material(false), + m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0), + m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0), + m_last_bed_temperature(0), m_last_bed_temperature_reached(true) {} Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; } From 6cf747808c79c913665e8c03a7d811329d3d8290 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 15:44:18 -0500 Subject: [PATCH 29/75] Belt: never start a scarf joint seam below the layer A scarf joint begins one layer height below the current layer and ramps up along the wall. On a tilted belt that start is a step backwards along the belt axis, into the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees. With an aligned seam the nozzle rams the same spot on every layer. A BabyBelt Pro benchy with seam_slope_type=external showed 601 such back-steps from layer 107 on, and in the field the belt "jumped backwards" and the head knocked the part loose. Belt printers now skip the scarf in GCode::extrude_loop, and the process tab greys the scarf controls out for them, as it already does for arc fitting. The regression test slices a cube on a belt with the scarf enabled and checks the belt axis never steps back by a layer pitch. --- src/libslic3r/GCode.cpp | 7 +++- src/slic3r/GUI/ConfigManipulation.cpp | 6 ++-- tests/fff_print/test_print.cpp | 51 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ed2dd400d4..fae8860b7b 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -7661,8 +7661,13 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref, loop.split_at(last_pos, false); const auto seam_scarf_type = m_config.seam_slope_type.value; + // Belt printers never get a scarf joint. The scarf starts one layer height + // below the layer, which on a tilted belt is a step backwards along the belt + // axis into the previous layer's wall at the seam (0.28 mm at 45 degrees per + // 0.2 mm layer); with an aligned seam that ram repeats at the same spot on + // every layer and knocks the part loose. bool enable_seam_slope = ((seam_scarf_type == SeamScarfType::External && !is_hole) || seam_scarf_type == SeamScarfType::All) && - !m_config.spiral_mode && + !m_config.spiral_mode && !m_config.belt_printer.value && (loop.role() == erExternalPerimeter || (loop.role() == erPerimeter && m_config.seam_slope_inner_walls)) && layer_id() > 0; const auto nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 65c2df092c..ac3ffa9006 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -1193,8 +1193,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("small_area_infill_flow_compensation_model", have_small_area_infill_flow_compensation); - toggle_field("seam_slope_type", !has_spiral_vase); - bool has_seam_slope = !has_spiral_vase && config->opt_enum("seam_slope_type") != SeamScarfType::None; + // Belt printers: the scarf would start one layer back along the belt, inside the + // previous layer (GCode::extrude_loop skips it there too). + toggle_field("seam_slope_type", !has_spiral_vase && !is_belt_printer); + bool has_seam_slope = !has_spiral_vase && !is_belt_printer && config->opt_enum("seam_slope_type") != SeamScarfType::None; toggle_line("seam_slope_conditional", has_seam_slope); toggle_line("seam_slope_start_height", has_seam_slope); toggle_line("seam_slope_entire_loop", has_seam_slope); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 78d8e0fd72..511a627fc1 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -20,6 +20,7 @@ #include "test_utils.hpp" #include +#include #include #include @@ -508,3 +509,53 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos); } } + +// A scarf joint starts one layer height below the layer and ramps up along the +// wall. On a tilted belt that start is a step backwards along the belt axis, into +// the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees. +// With an aligned seam the nozzle rams the same spot on every layer (field report +// from a BabyBelt Pro: the belt "jumped backwards" and knocked the part loose). +// Belt printers therefore never get a scarf, whatever the process preset says. +TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][belt][Seam]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "wall_loops", 2 }, + { "seam_position", "back" }, + { "seam_slope_type", "external" }, + { "seam_slope_inner_walls", 1 }, + { "seam_slope_start_height", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The belt axis is machine Z. Within a layer it only drifts by the frame + // coupling (well under 0.1 mm across a 20 mm cube); a scarf start is a full + // layer pitch (0.283 mm) backwards. + double last_z = std::numeric_limits::lowest(); + double worst_backstep = 0.; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (! line.cmd_is("G1") || ! line.has_z()) + return; + const double z = line.z(); + if (last_z != std::numeric_limits::lowest()) + worst_backstep = std::max(worst_backstep, last_z - z); + last_z = z; + }); + CHECK(worst_backstep < 0.2); +} From 48af5b5d9725281983dfcf9dc60e0e15dd754212 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 15:44:18 -0500 Subject: [PATCH 30/75] Read the pre-slice remap header tags at their real length The header tags lost their belt_ prefix in the Part 3.2 rename (20 characters now), but the parser still skipped 25, so every axis read as pos_x. Found in Hanif Koh's review of #14394. --- src/libslic3r/GCode/GCodeProcessor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index efb4a45ed0..88ce95195f 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4232,13 +4232,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers return RemapAxis::PosX; }; if (boost::starts_with(comment, " preslice_remap_x = ")) { - m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } if (boost::starts_with(comment, " preslice_remap_y = ")) { - m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } if (boost::starts_with(comment, " preslice_remap_z = ")) { - m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return; + m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(20)))); return; } } // wipe start tag From 2c0570c97d5ad6aaec27226dcc38649a099994c0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 15:44:49 -0500 Subject: [PATCH 31/75] Drop references to planning docs that are not in the tree The MachineKinematics comments pointed at docs/superpowers plan files, which are gitignored working notes. --- src/libslic3r/GCode/MachineKinematics.hpp | 2 +- tests/fff_print/test_gcodewriter.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode/MachineKinematics.hpp b/src/libslic3r/GCode/MachineKinematics.hpp index e046dd9476..5c53bd9f58 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -15,7 +15,7 @@ namespace Slic3r { // // This is a seam for writer-generated movement only. Start/end/custom G-code, // classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass -// through it; see doc in 09-orca-machinekinematics-split-plan.md section 6. +// through it: they write machine coordinates directly. class MachineKinematics { public: diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index a37a4947ae..b8f2b890e5 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -1038,7 +1038,7 @@ SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][bel // --------------------------------------------------------------------------- // Regression tests for the two latent bugs the MachineKinematics refactor -// preserved deliberately (see 09b-latent-bug-fix-plan.md). +// preserved deliberately and the follow-up commit fixed. // --------------------------------------------------------------------------- // Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every From 69a08915d10044d1ceae00fa0d1d59be64b08837 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 17:29:33 -0500 Subject: [PATCH 32/75] Belt: do not fail the G-code height check against belt travel check_multi_extruder_gcode_valid() compares each object's max Z with printable_height. On a belt printer machine Z is belt travel (a 3DBenchy on the BabyBelt Pro runs from Z=197 to Z=309 on a 69 mm printable_height), so every belt export set the over-height error bit and the CLI refused the plate with -102 "G-code in unprintable area". The preview already skips its ToolHeightOutside warning for the same reason; the export check now does too. The XY printable-area check is unchanged. --- src/libslic3r/GCode/GCodeProcessor.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 88ce95195f..c2aedd8262 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2860,7 +2860,11 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int valid = false; } } - if ( iter->second.max_print_z > plate_printable_height ) { //over height + // Belt printers: machine Z is belt travel, which grows without bound over a + // print, while printable_height is the clearance above the belt; the two are + // not comparable, so the over-height check is skipped, as the preview's + // ToolHeightOutside warning already is. + if ( !machine_frame_active && iter->second.max_print_z > plate_printable_height ) { //over height m_result.gcode_check_result.error_code |= (1 << 3); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -2901,7 +2905,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int } // check printable height - if ((extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { + if (!machine_frame_active && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { m_result.gcode_check_result.error_code |= (1 << 1); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; From 4f110bc26133516508e54bf4842762267a43e22e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 30 Sep 2026 17:42:16 -0500 Subject: [PATCH 33/75] Belt scarf test: slice without a z-hop The default 0.4 mm z-hop is a 0.57 mm move along the belt axis and its return tripped the back-step check. Shipped belt profiles print without a z-hop, so the test does too. --- tests/fff_print/test_print.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 511a627fc1..8b342fde68 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -537,6 +537,9 @@ TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][bel { "seam_slope_type", "external" }, { "seam_slope_inner_walls", 1 }, { "seam_slope_start_height", 0 }, + // No z-hop: on a belt a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm) + // and its return would read as a back-step. The shipped belt profiles print without one. + { "z_hop", 0 }, { "machine_start_gcode", "T[initial_tool]\n" }, { "layer_change_gcode", "G92 E0\n" }, }); From d57549159dda3cf727ec46f09d576c39cf4f458f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 25 Sep 2026 01:54:17 -0500 Subject: [PATCH 34/75] 3MF loader: refuse non-finite vertex coordinates A 3MF vertex with a nan/inf coordinate was accepted by both parsers and crashed qhull in ModelVolume's convex hull while the file was still loading. Both vertex handlers refuse it, and volume generation checks again whichever parser produced the geometry. The main parser's _stop_object_xml_parser keeps a message a handler already set. --- src/libslic3r/Format/bbs_3mf.cpp | 43 +++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index e2091da1db..366104bfa8 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -978,10 +978,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _stop_object_xml_parser(const std::string& msg = std::string()) { assert(! obj_parse_error); - assert(obj_parse_error_message.empty()); assert(object_xml_parser != nullptr); obj_parse_error = true; - obj_parse_error_message = msg; + if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already + obj_parse_error_message = msg; XML_StopParser(object_xml_parser, false); } @@ -3815,11 +3815,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { // appends the vertex coordinates // missing values are set equal to ZERO - if (m_curr_object) - m_curr_object->geometry.vertices.emplace_back( - m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), - m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), - m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + if (m_curr_object) { + const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), + m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), + m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + // A non-finite coordinate ("nan", "inf") used to be accepted and crashed + // qhull in ModelVolume's convex hull while the file was still loading. Refuse the file. + if (! v.allFinite()) { + _stop_xml_parser("Invalid vertex coordinate: not a finite number"); + return true; // the parser is stopped; returning false would overwrite the message + } + m_curr_object->geometry.vertices.emplace_back(v); + } return true; } @@ -5109,6 +5116,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } } + for (const Vec3f &v : sub_object->geometry.vertices) + if (! v.allFinite()) { // Qhull cannot take a NaN vertex + add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id)); + return false; + } its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end()); // BBS @@ -5600,11 +5612,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { // appends the vertex coordinates // missing values are set equal to ZERO - if (current_object) - current_object->geometry.vertices.emplace_back( - object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), - object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), - object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + if (current_object) { + const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR), + object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR), + object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR)); + // See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate + // crashed qhull while the file loaded. The dispatcher stops this parser on `false`. + if (! v.allFinite()) { + obj_parse_error_message = "Invalid vertex coordinate: not a finite number"; + return false; + } + current_object->geometry.vertices.emplace_back(v); + } return true; } From 6be7911e40564290e0c6a62617f95776a4c6dfd6 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 25 Sep 2026 01:54:17 -0500 Subject: [PATCH 35/75] CLI: survive a project with missing printable_height or fewer filaments than loaded Found by fuzzing the headless slicer: - A BBS-style 3MF without Metadata/project_settings.config segfaulted the CLI silently on the missing printable_height option. - A project saved with fewer filaments (or filament groups) than --load-filaments overran the filament variant tables (segfault in the variant match) and then hit an uncaught ConfigurationError from set_with_restore_2 (std::terminate). The tables are regenerated for the filaments the project did not know about, the destination vectors grown first (only from a non-empty source), the match bounded, and a failure becomes a CLI config error. --- src/OrcaSlicer.cpp | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 15c5677485..1abd1627d8 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -1809,7 +1809,10 @@ int CLI::run(int argc, char **argv) old_printable_width = static_cast(old_printable_bbox.size().x()); old_printable_depth = static_cast(old_printable_bbox.size().y()); } - old_printable_height = (int)(config.opt_float("printable_height")); + // A BBS-style 3mf without Metadata/project_settings.config has no + // printable_height (found by fuzzing: this was a silent segfault). + if (const auto *ph = config.option("printable_height")) + old_printable_height = (int) ph->value; if (config.option("extruder_clearance_height_to_rod")) old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod"); @@ -3341,9 +3344,14 @@ int CLI::run(int argc, char **argv) max_self_index = std::max(max_self_index, v); min_self_index = std::min(min_self_index, v); } - if (max_self_index > filament_count || min_self_index < 1) { - BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating") - % min_self_index % max_self_index % filament_count; + // And a project saved with FEWER filaments than are now loaded (a + // one-filament project sliced with two --load-filaments) leaves the tables half filled: + // the variant matching below then reads past filament_extruder_variant and + // set_with_restore_2 throws an uncaught size error. Regenerate in that case too. + if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count + || (int) filament_self_index_opt->values.size() < filament_count) { + BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating") + % min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size(); need_regenerate_self_index = true; } } @@ -3424,6 +3432,10 @@ int CLI::run(int argc, char **argv) std::vector& filament_variants = curr_variant_opt->values; filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard)); } + // See the filament_self_index note above: one variant per filament for + // the filaments the project did not know about. + if ((int) curr_variant_opt->values.size() < filament_count) + curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard)); const ConfigOptionStrings *new_variant_opt = dynamic_cast(config.option("filament_extruder_variant", true)); std::vector new_variant_indice; @@ -3432,7 +3444,7 @@ int CLI::run(int argc, char **argv) for (int i = 0; i < new_variant_count; i++) { - for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++) + for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++) { if (curr_variant_opt->values[j] == new_variant_opt->values[i]) { new_variant_indice[i] = j; @@ -3484,7 +3496,18 @@ int CLI::run(int argc, char **argv) ConfigOptionVectorBase* opt_vec_dst = static_cast(opt); const ConfigOptionVectorBase* opt_vec_src = static_cast(source_opt); //set with index - opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count); + try { + // A project with fewer filaments than are loaded: grow the + // destination to the filament's slot first (set_with_restore_2 only restores). + if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) + opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src); + opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count); + } catch (const std::exception &ex) { // Was an uncaught abort + BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what(); + boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl; + record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info); + flush_and_exit(CLI_CONFIG_FILE_ERROR); + } } continue; @@ -3530,7 +3553,16 @@ int CLI::run(int argc, char **argv) if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) { std::vector temp_variant_indice; temp_variant_indice.resize(new_variant_count, -1); - opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true); + try { + if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above + opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src); + opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true); + } catch (const std::exception &ex) { // Was an uncaught abort + BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what(); + boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl; + record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info); + flush_and_exit(CLI_CONFIG_FILE_ERROR); + } if (opt_key == "filament_extruder_variant") new_variant_counts[filament_index - 1] = opt_vec_src->size(); From 6a6ceb2612211321711984ba6e57e9131a7823c3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 23:39:13 -0500 Subject: [PATCH 36/75] G-code: key the island tour cache on the island layout, never index past the islands The per-filament island tour was cached by island centroids only. A later layer with the same centroids but fewer islands (thin walls, negative volumes) reused the stale visit list, whose catch-all index pointed past the layer's islands, and extrude_perimeters read freed memory (three fuzz crashes, planar and belt). The per-instance island layout is part of the cache key and the use site never indexes past the islands. --- src/libslic3r/GCode.cpp | 21 +++++++++++++++------ src/libslic3r/GCode.hpp | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fae8860b7b..75b14e06bc 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -6491,6 +6491,7 @@ LayerResult GCode::process_layer( std::vector &objects_by_extruder = objects_by_extruder_it->second; std::vector &instances = filament_plan.first; std::vector nodes; + std::vector> layout; // Per instance, see IslandOrderCacheEntry std::vector node_instances; auto quantize_to_mm = [](const Point &pt) -> Point { const coord_t grid = coord_t(scale_(1.)); @@ -6515,6 +6516,7 @@ LayerResult GCode::process_layer( const size_t instance_idx = instances.size(); instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id, print_object->instances()[instance_id].model_instance->get_labeled_id()); + layout.emplace_back(islands.size(), ! islands.empty() && ! islands.back().by_region.empty()); const Point &shift = print_object->instances()[instance_id].shift; const size_t first_node = nodes.size(); if (islands_chainable) @@ -6534,8 +6536,9 @@ LayerResult GCode::process_layer( // Reuse the cached tour while this filament's island layout is unchanged. auto &cache_entry = m_ordering_cache[filament_id]; - if (!(cache_entry.first == nodes)) { - cache_entry.first = nodes; + if (! (cache_entry.nodes == nodes && cache_entry.layout == layout)) { + cache_entry.nodes = nodes; + cache_entry.layout = layout; Points node_points; node_points.reserve(nodes.size()); for (const IslandOrderNode &node : nodes) @@ -6568,12 +6571,12 @@ LayerResult GCode::process_layer( // A visit without explicit islands already prints everything. continue; std::vector &islands = instances[i].object_by_extruder.islands; - if (!islands.back().by_region.empty()) + if (! islands.empty() && ! islands.back().by_region.empty()) last_visit.islands.emplace_back(islands.size() - 1); } - cache_entry.second = std::move(visits); + cache_entry.visits = std::move(visits); } - filament_plan.second = cache_entry.second; + filament_plan.second = cache_entry.visits; } } @@ -6896,7 +6899,13 @@ LayerResult GCode::process_layer( // in this instance's frame after set_origin() above). Empty islands are skipped; // the trailing catch-all island has no centroid to chain by and always goes last. std::vector &islands = instance_to_print.object_by_extruder.islands; - std::vector island_order = visit.islands; + std::vector island_order; + island_order.reserve(visit.islands.size()); + for (size_t idx : visit.islands) // Never index past the islands (see IslandOrderCacheEntry) + if (idx < islands.size()) + island_order.emplace_back(idx); + else + BOOST_LOG_TRIVIAL(error) << "island tour refers to island " << idx << " of " << islands.size() << ", skipped"; if (island_order.empty()) { island_order.reserve(islands.size()); if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index e62d9cee41..5d99c219c6 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -632,9 +632,21 @@ protected: }; // Cache the per-filament island tour to avoid recomputing while the layer's island layout is - // unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}. - std::map, std::vector>> - m_ordering_cache; + // unchanged. Key: filament_id. Value: the nodes the tour was computed from, the per-instance + // island layout (count and whether the trailing catch-all island has anything to print), and + // the resulting visits. + // The layout is part of the key. Nodes only cover the chainable islands, so two + // layers with the same centroids but a different number of islands (thin walls, negative + // volumes come and go) matched the cache and the visit's catch-all index -- islands.size() - 1 + // of the OLD layer -- ran past the new layer's islands (found by fuzzing: segfault in + // extrude_perimeters on multi-part objects). + struct IslandOrderCacheEntry + { + std::vector nodes; + std::vector> layout; + std::vector visits; + }; + std::map m_ordering_cache; ExtrusionQualityEstimator m_extrusion_quality_estimator; From 2ae41bb0efe1bd2c421987f32270cac5dc7df238 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 23:39:13 -0500 Subject: [PATCH 37/75] Painting: guard the top/bottom projection erase when no shell layers are requested With top_shell_layers = 0 the `top` vector is never filled and erasing its begin() was undefined (found by fuzzing on a painted object dropped below the plate). --- src/libslic3r/MultiMaterialSegmentation.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/MultiMaterialSegmentation.cpp b/src/libslic3r/MultiMaterialSegmentation.cpp index 0016d96ac2..67b431790e 100644 --- a/src/libslic3r/MultiMaterialSegmentation.cpp +++ b/src/libslic3r/MultiMaterialSegmentation.cpp @@ -1244,10 +1244,16 @@ static inline std::vector> segmentation_top_and_bottom_l slicing_params.trafo = volume_trafo; Polygons bottom_slice = slice_mesh(painted, zs[0], slicing_params); - top.erase(top.begin()); - bottom.erase(bottom.begin()); - - bottom[0] = union_(bottom[0], bottom_slice); + // Only the requested projections exist: with + // top_shell_layers = 0 `top` is empty and erasing its begin() was + // undefined (found by fuzzing: a sunk, painted object crashed here). + if (! top.empty()) + top.erase(top.begin()); + if (! bottom.empty()) { + bottom.erase(bottom.begin()); + if (! bottom.empty()) + bottom[0] = union_(bottom[0], bottom_slice); + } } else slice_mesh_slabs(painted, zs, volume_trafo, max_top_layers > 0 ? &top : nullptr, max_bottom_layers > 0 ? &bottom : nullptr, nullptr, throw_on_cancel_callback); auto merge = [](std::vector &&src, std::vector &dst) { From e8926efbe33430680ce1d33c511c8029720036ef Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 19:42:44 -0500 Subject: [PATCH 38/75] Belt purge: detect filament changes by scanning the ordering, not the first layer's flag ToolOrdering::has_wipe_tower() reads the first layer's flag. On a belt the first layer may be a brim apron band, which carries neither object nor support and never gets the flag, so with a brim the purge plan returned early and nothing was purged. Scan the layers for a change. --- src/libslic3r/BeltPurge.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index 37d89ca14e..bba338b878 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -165,9 +165,18 @@ void Print::_plan_belt_purge() if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); - if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) - // No toolchanges anywhere, nothing to purge. - return; + // Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the + // FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries + // neither object nor support and so never gets the flag even when the print changes filament. + { + bool any_change = false; + unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur) { any_change = true; cur = e; } + if (! any_change) + return; + } this->throw_if_canceled(); From d755580c994a41fe2f40ecf360182a9a2a3750bb Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 19:39:01 -0500 Subject: [PATCH 39/75] Belt brim: the purge prism never gets a brim, whatever its config says The prism's generator already sets no_brim; PrintObject::has_belt_brim() now also ignores any brim setting on the prism (belt_purge_tower_object), so a brim on the parts beside it never blocks purging. --- src/libslic3r/PrintObject.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 81b8a58d6b..58ce53df5a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1209,6 +1209,10 @@ bool PrintObject::has_belt_brim() const { if (! m_print->has_tilted_belt()) return false; + // The purge prism is sacrificial and sits at the plate's edge; its generator sets no_brim, and + // this keeps it brimless whatever its config says, so a brim on the parts never blocks purging. + if (m_config.belt_purge_tower_object.value) + return false; if (! this->belt_brim_instances_compatible()) return false; if (m_config.brim_type == btNoBrim) From b8a8e38ceb5898191db2d61fdb4e89e878635fd2 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 25 Sep 2026 01:54:17 -0500 Subject: [PATCH 40/75] G-code: no crossing-perimeter avoidance while travelling on a brim apron layer Crossing-perimeter avoidance dereferenced the (null) layer while travelling on a brim apron layer. --- src/libslic3r/GCode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 75b14e06bc..9947e40b8d 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -9427,6 +9427,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // multi-hop travel path inside the configuration space if (m_config.reduce_crossing_wall && !m_avoid_crossing_perimeters.disabled_once() + && m_layer != nullptr // A brim apron layer has no Layer to avoid crossing && m_writer->is_current_position_clear()) //BBS: don't generate detour travel paths when current position is unclea { From f219431935cf95c499cf3bdda1b1e7a40d83c913 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 24 Sep 2026 07:39:43 -0500 Subject: [PATCH 41/75] GUI: the plate is open along Y for containment tests on an infinite-Y belt printer PartPlate's containment tests treat the plate as open along Y on a belt printer with belt_printer_infinite_y, so a long part is no longer flagged outside the plate in Prepare while the slicer and the G-code checks accept it. The check reads the printer preset through the app object, which does not exist headlessly, so it is guarded on the plater. --- src/slic3r/GUI/PartPlate.cpp | 16 ++++++++++++++++ src/slic3r/GUI/PartPlate.hpp | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 893260f934..12f4e423e5 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -2783,6 +2783,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi BoundingBoxf3 instance_box = bounding_box? *bounding_box: object->instance_convex_hull_bounding_box(instance_id); Polygon hull = instance->convex_hull_2d(); BoundingBoxf3 plate_box = get_plate_box(); + this->open_belt_y(plate_box); if (instance_box.max.z() > plate_box.min.z()) plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking @@ -3467,6 +3468,19 @@ Polygon PartPlate::get_shared_printable_polygon() const return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas); } + +bool PartPlate::belt_open_y() const +{ + // Headless (CLI) plates have no plater and no wxApp behind wxGetApp(); the CLI's own belt + // handling lives in Print::validate(). + if (m_plater == nullptr || wxGetApp().preset_bundle == nullptr) + return false; + const DynamicPrintConfig &printer = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto *belt = printer.option("belt_printer"); + const auto *infinite_y = printer.option("belt_printer_infinite_y"); + return belt != nullptr && belt->value && infinite_y != nullptr && infinite_y->value; +} + bool PartPlate::contains(const Vec3d& point) const { return m_bounding_box.contains(point); @@ -3486,6 +3500,7 @@ bool PartPlate::contains(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.contains(bb); } @@ -3498,6 +3513,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon; print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon; print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon; + this->open_belt_y(print_volume); return print_volume.intersects(bb); } diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index e913ebaabf..babc88f364 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -427,6 +427,10 @@ public: bool contains(const GLVolume& v) const; bool contains(const BoundingBoxf3& bb) const; bool intersects(const BoundingBoxf3& bb) const; + // A belt printer with belt_printer_infinite_y: the plate is open along Y for the + // containment tests (the drawn plate keeps its shape). + bool belt_open_y() const; + void open_belt_y(BoundingBoxf3 &box) const { if (this->belt_open_y()) { box.min.y() = -1e5; box.max.y() = 1e5; } } void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true); From 33fc99785a45184d794014bd38ad920d4c46c299 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:02:57 -0500 Subject: [PATCH 42/75] Belt profiles: print without a z-hop by default On a belt printer a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm of belt travel out and back on every hop), not a lift away from the part. The three belt printer bases now ship z_hop 0, the IR3 V2 leaf no longer restates 0.4, and the BabyBelt Pro and IR3 V2 filaments stop overriding the printer with filament_z_hop 0.4. The option stays editable. --- resources/profiles/Custom.json | 2 +- resources/profiles/Custom/machine/fdm_belt_common.json | 2 +- resources/profiles/IdeaFormer.json | 2 +- .../IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json | 3 --- .../IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json | 3 --- .../IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json | 3 --- resources/profiles/IdeaFormer/machine/fdm_belt_common.json | 2 +- resources/profiles/Printcepts.json | 2 +- .../Printcepts/filament/Generic PETG @BabyBelt Pro.json | 3 --- .../Printcepts/filament/Generic PLA @BabyBelt Pro.json | 3 --- resources/profiles/Printcepts/machine/fdm_belt_common.json | 2 +- 11 files changed, 6 insertions(+), 21 deletions(-) diff --git a/resources/profiles/Custom.json b/resources/profiles/Custom.json index 365fed4b5b..4093fcedf4 100644 --- a/resources/profiles/Custom.json +++ b/resources/profiles/Custom.json @@ -1,6 +1,6 @@ { "name": "Custom Printer", - "version": "02.04.00.05", + "version": "02.04.00.06", "force_update": "0", "description": "My configurations", "machine_model_list": [ diff --git a/resources/profiles/Custom/machine/fdm_belt_common.json b/resources/profiles/Custom/machine/fdm_belt_common.json index 86245e7377..3c45b05515 100644 --- a/resources/profiles/Custom/machine/fdm_belt_common.json +++ b/resources/profiles/Custom/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index 4a8519353b..f9f32d2f3f 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.03", + "version": "02.00.00.04", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json index 22ec0737d7..709d0e9897 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PETG @IdeaFormer IR3 V2.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "40" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C" ] diff --git a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json index 74c2011bca..10a8ed97f9 100644 --- a/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json +++ b/resources/profiles/IdeaFormer/filament/Generic PLA @IdeaFormer IR3 V2.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "30" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C" ] diff --git a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json index 5f477ef0bb..ce02ce82d1 100644 --- a/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json +++ b/resources/profiles/IdeaFormer/machine/IdeaFormer IR3 V2 0.4 nozzle.json @@ -81,9 +81,6 @@ "deretraction_speed": [ "40" ], - "z_hop": [ - "0.4" - ], "retract_lift_below": [ "300" ], diff --git a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json index 25daeb34a7..60ef096de0 100644 --- a/resources/profiles/IdeaFormer/machine/fdm_belt_common.json +++ b/resources/profiles/IdeaFormer/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index 61d418b8ab..1abe84873b 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.01", + "version": "01.00.00.02", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json index 939edbf233..5bfd8e7f2d 100644 --- a/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PETG @BabyBelt Pro.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "40" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PETG @BabyBelt Pro — belt PETG, bed 80C" ] diff --git a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json index aa11e4c598..4637f21273 100644 --- a/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json +++ b/resources/profiles/Printcepts/filament/Generic PLA @BabyBelt Pro.json @@ -104,9 +104,6 @@ "filament_deretraction_speed": [ "30" ], - "filament_z_hop": [ - "0.4" - ], "filament_start_gcode": [ "; Generic PLA @BabyBelt Pro — belt PLA, bed 75C" ] diff --git a/resources/profiles/Printcepts/machine/fdm_belt_common.json b/resources/profiles/Printcepts/machine/fdm_belt_common.json index fe3a58272e..2a392b4f60 100644 --- a/resources/profiles/Printcepts/machine/fdm_belt_common.json +++ b/resources/profiles/Printcepts/machine/fdm_belt_common.json @@ -77,7 +77,7 @@ "1" ], "z_hop": [ - "0.4" + "0" ], "z_hop_types": [ "Normal Lift" From 93fc4b31e9ac8da98575114304dc8e43dbeb1b4d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:02:58 -0500 Subject: [PATCH 43/75] Belt: show the axis remap options in Develop mode only preslice_remap_*, preslice_remap_global and gcode_remap_* describe the printer's kinematics and are set once by its profile. A wrong value sends the gantry outside the machine (a user preset with the pre-slice remap in place of the G-code remap emitted gantry moves to Y=646 mm), so they are no longer offered in Expert mode. --- src/libslic3r/PrintConfig.cpp | 14 +++++++------- src/slic3r/GUI/Tab.cpp | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a6a419a15e..cc17d0fdec 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7281,19 +7281,19 @@ void PrintConfigDef::init_fff_params() "your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. " "Default +X: no change.", - RemapAxis::PosX, comExpert); + RemapAxis::PosX, comDevelop); add_belt_remap("preslice_remap_y", "Y", "Before slicing, which model-space axis becomes the slicer's Y axis. " "The slicer treats Y as one of the two horizontal bed axes. If your physical " "belt surface runs along the Z axis, map Y to +Z here so the slicer slices " "along the correct plane. Default +Y: no change.", - RemapAxis::PosY, comExpert); + RemapAxis::PosY, comDevelop); add_belt_remap("preslice_remap_z", "Z", "Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " "The slicer builds layers upward along this axis. If your printer's layer-stacking " "direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). " "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.", - RemapAxis::PosZ, comExpert); + RemapAxis::PosZ, comDevelop); def = this->add("preslice_remap_global", coBool); def->label = L("Global"); @@ -7302,12 +7302,12 @@ void PrintConfigDef::init_fff_params() "Without this, the remap is applied locally around each object's center, so " "objects at different positions don't get a position-dependent contribution. " "Mirrors the 'Global' option on the belt slicing rotation, but for the remap."); - def->mode = comExpert; + def->mode = comDevelop; def->set_default_value(new ConfigOptionBool(false)); - add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comExpert); - add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comExpert); - add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comExpert); + add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comDevelop); + add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comDevelop); + add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop); // The machine-frame G-code transform (shear + scale) is no longer configured // by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9905a203ba..b7c9264381 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6214,8 +6214,10 @@ void TabPrinter::toggle_options() toggle_line("belt_slice_rotation", is_belt); // Remap, back-transform, and global mesh-transforms toggles are gated by belt - // mode here; finer mode-based visibility (Advanced vs Expert) is handled by - // each option's ConfigOptionMode in PrintConfig.cpp. + // mode here; finer mode-based visibility is handled by each option's + // ConfigOptionMode in PrintConfig.cpp. Both axis remaps are Develop-only: a + // printer profile sets them once for its kinematics, and a wrong value sends + // the gantry outside the machine. for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"}) toggle_line(el, is_belt); toggle_line("belt_preslice_global", is_belt); From 3776739cb5f8f048d136c97d5df3692e86b0186f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:02:58 -0500 Subject: [PATCH 44/75] docs: drop the private build notification note The build-notify workflow is a local tool of one contributor and does not belong in the shared agent instructions. --- AGENTS.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ed0ec8121b..4be195b40d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,11 +15,6 @@ cmake --build build --config RelWithDebInfo --target all -- cmake --build . --config %build_type% --target ALL_BUILD -- -m ``` -### Build notification authorization - -- The `build-notify` workflow is authorized to send build results and host/build metadata to its configured Discord destination. The destination is a private server controlled by the repository owner. -- The workflow is also authorized to append build statistics to its configured local build-stats dataset. - ## Testing Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow. From 97034b22f6af927d2b665635ca376c6fc400197f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:04:33 -0500 Subject: [PATCH 45/75] Belt: remove the tagged diagnostic logging Drops the [BELT-DEBUG], [BELTRACE], [BELT-CALIB] and [BELT-PREVIEW] log lines, the SLIC3R_BELT_DIAGNOSTIC_LOG blocks, and the counters and temporaries that existed only to feed them. Six of the purge tower lines logged at warning level, which is Orca's default, on every plan. Raised in Hanif Koh's review of #14394. --- src/libslic3r/BeltPurge.cpp | 44 +-------------- src/libslic3r/BeltSliceStrategy.cpp | 80 +-------------------------- src/libslic3r/PrintObject.cpp | 13 +---- src/libslic3r/PrintObjectSlice.cpp | 66 +--------------------- src/libslic3r/Support/TreeSupport.cpp | 2 - src/slic3r/GUI/BeltPurgeTower.cpp | 19 ------- src/slic3r/GUI/GCodeViewer.cpp | 2 - src/slic3r/GUI/Plater.cpp | 14 ----- 8 files changed, 5 insertions(+), 235 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index bba338b878..cff3de8846 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -128,10 +128,6 @@ void Print::_align_belt_purge_layers() delta += h; po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0) } - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size() - << " objects onto ref grid offset=" << ref_offset - << " (ref=" << ref->model_object()->name << ")"; } // Belt mode replacement for _make_wipe_tower(): plan filament-change purging @@ -246,21 +242,11 @@ void Print::_plan_belt_purge() } } - // Diagnostic: the prism only absorbs purge at toolchange layers whose - // print_z coincides with one of its own layers. Compare the prism's layer - // print_z range to the toolchange print_z range and count how many - // toolchange layers actually land on a prism layer. This distinguishes a - // range/grid-alignment failure (no coverage) from a capacity shortfall - // (covered but not enough cross-section). + // The prism only absorbs purge at toolchange layers whose print_z coincides + // with one of its own layers. PrintObject *prism_po = nullptr; for (PrintObject *po : m_objects) if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; } - const PrintObject *diag_prism = prism_po; - if (diag_prism != nullptr) - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" - << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z - << "] nlayers=" << diag_prism->layers().size(); - int tc_layers = 0, tc_layers_covered = 0; float total_leftover = 0.f; float worst_layer_leftover = 0.f; @@ -269,26 +255,14 @@ void Print::_plan_belt_purge() unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { float layer_leftover = 0.f; - bool layer_has_tc = false; for (const unsigned int extruder_id : layer_tools.extruders) { if (extruder_id == current_extruder_id) continue; - if (!layer_has_tc) { - layer_has_tc = true; - ++tc_layers; - if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) - ++tc_layers_covered; - } float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, volume_to_wipe); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover; layer_leftover += leftover; current_extruder_id = extruder_id; } @@ -323,11 +297,6 @@ void Print::_plan_belt_purge() this->throw_if_canceled(); } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers - << " toolchange layers land on a prism layer" - << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : - tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); - if (total_leftover > 1.f) { this->active_step_add_warning( PrintStateBase::WarningLevel::CRITICAL, @@ -336,8 +305,6 @@ void Print::_plan_belt_purge() "Increase the belt purge tower width, or reduce flushing volumes."), int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover - << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; } } @@ -356,10 +323,6 @@ void PrintObject::belt_shift_layer_grid(double delta) for (SupportLayer *layer : m_support_layers) layer->print_z += delta; m_slicing_params.belt_floor_z_shift += delta; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" - << " obj=" << this->model_object()->name - << " delta=" << delta - << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); } // Belt mode: drop layers strictly above z (used to cancel the purge prism early @@ -382,9 +345,6 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z) m_layers.resize(keep); if (!m_layers.empty()) m_layers.back()->upper_layer = nullptr; - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z - << " kept=" << keep << " removed=" << removed - << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); return removed; } diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index cdb6ba428f..d16819e7db 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -3,13 +3,6 @@ #include -#include -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG -#include -#include -#include -#endif - namespace Slic3r { void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, @@ -45,79 +38,18 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // each volume within the object) would compute min_z against mesh-local vertex // coordinates rather than object-space coordinates, so volumes translated along // the slicer's Z axis would be silently excluded from the bound check. -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Capture the incoming trafo for diagnostic logging. - // This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift. - const Transform3d trafo_pre_shift = trafo; - auto log_mat = [](const Matrix3d &m) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "]," - << "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "]," - << "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]"; - return ss.str(); - }; - auto log_vec3 = [](const Vec3d &v) { - std::ostringstream ss; - ss << std::fixed << std::setprecision(4); - ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")"; - return ss.str(); - }; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter" - << " has_rotation=" << has_rotation - << " has_remap=" << has_remap - << " trafo.linear=" << log_mat(trafo_pre_shift.linear()) - << " trafo.translation=" << log_vec3(trafo_pre_shift.translation()) - << " volumes=" << model_volumes.size(); -#endif double min_z = std::numeric_limits::max(); -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - int vol_idx = 0; -#endif for (const ModelVolume *mv : model_volumes) { -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - if (!mv->is_model_part()) { ++vol_idx; continue; } -#else - if (!mv->is_model_part()) continue; -#endif Transform3d vol_trafo = trafo * mv->get_matrix(); const auto &its = mv->mesh().its; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - // Per-volume bbox in mesh-frame and post-trafo slicer-frame. - Vec3d mesh_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d mesh_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - Vec3d slicer_min(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); - Vec3d slicer_max(std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::lowest()); - double vol_min_z = std::numeric_limits::max(); -#endif for (const stl_vertex &v : its.vertices) { Vec3d vm = v.cast(); Vec3d pt = vol_trafo * vm; min_z = std::min(min_z, pt.z()); -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - mesh_min = mesh_min.cwiseMin(vm); - mesh_max = mesh_max.cwiseMax(vm); - slicer_min = slicer_min.cwiseMin(pt); - slicer_max = slicer_max.cwiseMax(pt); - vol_min_z = std::min(vol_min_z, pt.z()); -#endif } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max) - << " get_matrix.translation=" << log_vec3(mv->get_matrix().translation()) - << " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max) - << " vol_min_z=" << vol_min_z; - ++vol_idx; -#endif } const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits::max()) ? -min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z - << " z_shift_val=" << z_shift_val; -#endif if (z_shift_val > 0.) { Transform3d z_shift = Transform3d::Identity(); z_shift.matrix()(2, 3) = z_shift_val; @@ -126,18 +58,8 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, // out_belt_min_z is only meaningful in belt mode; the standalone-remap path // never reported it. if (out_belt_min_z && config.belt_printer.value) { - const double new_val = (min_z != std::numeric_limits::max()) ? min_z : 0.; -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id() - << " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val; -#endif - *out_belt_min_z = new_val; + *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; } -#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit" - << " final_trafo.linear=" << log_mat(trafo.linear()) - << " final_trafo.translation=" << log_vec3(trafo.translation()); -#endif } } // namespace Slic3r diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 58ce53df5a..6b9c5842bf 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4,7 +4,6 @@ #include "Print.hpp" #include "BeltTransform.hpp" -#include #include "BoundingBox.hpp" #include "ClipperUtils.hpp" #include "Clipper2Utils.hpp" @@ -460,15 +459,11 @@ std::vector> PrintObject::detect_extruder_geometric_unprintables() // 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal). void PrintObject::make_perimeters() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this; // prerequisites this->slice(); - if (! this->set_started(posPerimeters)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + if (! this->set_started(posPerimeters)) return; - } - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this; m_print->set_status(15, L("Generating walls")); BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info(); @@ -566,7 +561,6 @@ void PrintObject::make_perimeters() m_print->throw_if_canceled(); BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end"; - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posPerimeters); } @@ -955,9 +949,7 @@ void PrintObject::detect_overhangs_for_lift() void PrintObject::generate_support_material() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this; if (this->set_started(posSupportMaterial)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this; this->clear_support_layers(); if(!has_support() && !m_print->get_no_check_flag()) { @@ -1005,10 +997,7 @@ void PrintObject::generate_support_material() // posSupportMaterial, so this needs no extra invalidation edges. make_belt_brim(*this); m_print->throw_if_canceled(); - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this; this->set_done(posSupportMaterial); - } else { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; } } diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 8360a9562f..2181b3e0d5 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1,6 +1,5 @@ #include #include -#include #include @@ -860,12 +859,8 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay // Resulting expolygons of layer regions are marked as Internal. void PrintObject::slice() { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this; - if (! this->set_started(posSlice)) { - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)"; + if (! this->set_started(posSlice)) return; - } - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this; //BBS: add flag to reload scene for shell rendering m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE); std::vector layer_height_profile; @@ -1000,41 +995,6 @@ void PrintObject::slice() 0.); double centering_z_corr = (T_fwd.linear() * c_off).z(); global_z_offset += centering_z_corr; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] centering correction" - << " obj=" << this->model_object()->name - << " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")" - << " centering_z_corr=" << centering_z_corr - << " (added to global_z_offset)"; - } - - // [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared - // side-by-side. Lays out every value that feeds into the final layer - // print_z adjustment. - { - BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box(); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] slice() per-object summary" - << " obj=" << this->model_object()->name - << " n_volumes=" << this->model_object()->volumes.size() - << " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")" - << " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")" - << " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")" - << " m_center_offset=(" << unscale(m_center_offset.x()) << "," << unscale(m_center_offset.y()) << ")" - << " inst_shift=(" << unscale(inst_shift.x()) << "," << unscale(inst_shift.y()) << ")" - << " m_belt_min_z=" << m_belt_min_z - << " belt_surface_z=" << belt_surface_z - << " belt_z_shift=" << belt_z_shift; - // Per-volume bbox + get_matrix translation so order/composition is visible. - int vi = 0; - for (const ModelVolume *mv : this->model_object()->volumes) { - if (!mv->is_model_part()) { ++vi; continue; } - BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix()); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vi - << "] id=" << mv->id().id << " name='" << mv->name << "'" - << " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")" - << " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")" - << " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")"; - ++vi; - } } if (pcfg.belt_preslice_global.value) { @@ -1044,9 +1004,6 @@ void PrintObject::slice() Vec3d d(unscale(inst_shift.x()), unscale(inst_shift.y()), 0.); Vec3d c = T.linear() * d - d; global_z_offset += c.z(); - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id() - << " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() - << ") new=(" << c.x() << "," << c.y() << ")"; m_belt_global_xy_correction = Vec2d(c.x(), c.y()); BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" @@ -1081,18 +1038,7 @@ void PrintObject::slice() BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset << " (relative to min across " << this->print()->objects().size() << " objects)"; - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id() - << " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset; m_belt_global_z_offset = global_z_offset; - // [BELT-DEBUG] Final breakdown of all contributions to layer.print_z - // and where the first / last layer end up post-adjustment. - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] global_z_offset breakdown" - << " obj=" << this->model_object()->name - << " belt_z_shift=" << belt_z_shift - << " total_global_z_offset=" << global_z_offset - << " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")" - << " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift) - << " n_layers=" << m_layers.size(); if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) layer->print_z += global_z_offset; @@ -1101,12 +1047,6 @@ void PrintObject::slice() // layer print_z, so belt_floor_z_shift must match. m_slicing_params.belt_floor_z_shift += global_z_offset; } - if (!m_layers.empty()) { - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] post-adjustment" - << " first_layer.print_z=" << m_layers.front()->print_z - << " last_layer.print_z=" << m_layers.back()->print_z - << " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift; - } if (!m_layers.empty()) { BOOST_LOG_TRIVIAL(trace) << "Belt global: first_layer_z=" << m_layers.front()->print_z << " last_layer_z=" << m_layers.back()->print_z @@ -1125,10 +1065,6 @@ void PrintObject::slice() } // BBS - BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this - << " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z - << " belt_global_z_offset=" << m_belt_global_z_offset - << " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"; this->set_done(posSlice); } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 7cf1baeb68..75195d0167 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1913,8 +1913,6 @@ void TreeSupport::generate() 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(); diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 1882363d27..a8b0b0c0b8 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -171,7 +171,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object if (prism_idxs.empty()) return false; remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; return true; } @@ -295,22 +294,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - BoundingBoxf bed_ext_dbg; if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { const BoundingBoxf bed_ext = get_extents(bed_opt->values); - bed_ext_dbg = bed_ext; const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; } - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" - << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" - << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max - << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() - << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" - << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, belt_is_y ? belt_center : lat_center, 0.5 * height); @@ -395,15 +385,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // until the parts/config actually change. sig = new_sig; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" - << " belt_is_y=" << belt_is_y - << " W=" << width << " L=" << length << " H=" << height - << " v_layer=" << v_layer << " max_flush=" << max_flush - << " filaments=" << filaments.size() - << " theta_deg=" << Geometry::rad2deg(theta) - << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" - << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," - << new_object->bounding_box_exact().center().y() << ")"; return true; } diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 58fe8f9931..9193cc5ef8 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1387,8 +1387,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // translation, which this min-corner step recovers. const Vec3d d = model_bb.min - tp_bb.min; belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv; - BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor d=[" << d.x() << "," << d.y() << "," << d.z() - << "] (clip kept " << n_clip << "/" << n_filtered << " moves)"; } } libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer, diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 69086cd915..5b6e2c6300 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15893,20 +15893,6 @@ void Plater::_calib_apply_belt_mode() 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 From 2aa4122aae3bf1b045400053c92454ad7c80c407 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:42 -0500 Subject: [PATCH 46/75] Belt: check object height against the gantry clearance again validate() skipped the build-volume height check whenever the machine-frame transform was active, which is every shipped belt profile, so a 400 mm object passed on a 300 mm printable_height. The transform only changes how the height is written to G-code; the clearance check from f682ab5cd3 applies regardless. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 13 +++---------- tests/fff_print/test_print.cpp | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index dc018388b2..8f70405307 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -20,7 +20,6 @@ #include "GCode.hpp" #include "BeltGCode.hpp" #include "BeltTransform.hpp" -#include "GCode/MachineFrameTransform.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" #include "GCode/WipeTowerEstimate.hpp" @@ -1621,17 +1620,11 @@ StringObjectException Print::validate(std::vector *warnin // is not comparable to printable_height (which is gantry clearance in the // build-volume frame). Compare against the model's pre-shear Z instead, // mirroring the bbox computed in PrintObject::update_slicing_parameters. - // When the post-gcode MachineFrameTransform is active the printer's - // physical Z mapping is non-trivial — skip the check entirely. + // The machine-frame transform only changes how that height is written to + // G-code, not how much room there is under the gantry. const bool belt_printer = this->config().belt_printer.value; - bool skip_max_height_check = false; - if (belt_printer) { - MachineFrameTransform machine_frame; - machine_frame.init_from_config(this->config()); - skip_max_height_check = machine_frame.is_active(); - } const double shrinkage_compensation_z = this->shrinkage_compensation().z(); - for (size_t print_object_idx = 0; !skip_max_height_check && print_object_idx < m_objects.size(); ++ print_object_idx) { + for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) { const PrintObject &print_object = *m_objects[print_object_idx]; double effective_max_z = 0; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8b342fde68..6f0eb7b00e 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -562,3 +562,37 @@ TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][bel }); CHECK(worst_backstep < 0.2); } + +// printable_height on a belt printer is the clearance under the gantry, so an object taller +// than that is refused whatever the machine-frame transform does to the emitted coordinates. +TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[Print][belt]") +{ + auto belt_config = [](double printable_height) { + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "printable_height", printable_height }, + { "skirt_loops", 0 }, + }); + return config; + }; + + SECTION("a 20 mm cube fits under 50 mm of clearance") { + Print print; + Model model; + init_print({ cube(20) }, print, model, belt_config(50)); + CHECK(print.validate().string.empty()); + } + SECTION("a 60 mm cube does not") { + Print print; + Model model; + init_print({ cube(60) }, print, model, belt_config(50)); + CHECK_FALSE(print.validate().string.empty()); + } +} From 3752144995e65de48d43f435d4f407613ca4c762 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:42 -0500 Subject: [PATCH 47/75] Belt: do not refuse a brim because the prime tower setting is on enable_prime_tower stays on for any multi-filament project, but a belt printer never prints the classic tower and the belt purge prism is an ordinary object that never takes a brim, so every brim on a multi-filament belt print was refused for nothing. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 7 +++---- tests/fff_print/test_skirt_brim.cpp | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 8f70405307..5bfdfe8eac 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1420,11 +1420,10 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // neither the prime tower nor spiral vase can share. + // spiral vase cannot share. The prime tower setting is no obstacle: belt + // printers never print the classic tower, and the belt purge prism is an + // ordinary object that never takes a brim. if (this->has_belt_brim()) { - if (m_config.enable_prime_tower.value) - return { L("Brim is not compatible with the prime tower on a belt printer. " - "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index a34d28abae..5fa810106f 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -919,12 +919,15 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK_FALSE(print.objects().front()->has_belt_brim()); CHECK(print.validate().string.empty()); } - SECTION("a real inner brim still rejects the prime tower") { + // enable_prime_tower stays on for any multi-filament project, but a belt printer never + // prints the classic tower, so the setting alone must not cost the print its brim. + SECTION("a real inner brim is accepted with the prime tower setting on") { Print print; Model model; init_inner_leading_with_prime_tower(print, model, 4); CHECK(print.objects().front()->has_belt_brim()); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); } } From c49e8d32c8a888e7b6b09a8ba58c01091a79c08c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:43 -0500 Subject: [PATCH 48/75] Belt: drop the per-extrusion transform determinant The mesh transform is a rotation and an axis permutation, so its determinant is always 1; rebuilding the forward transform on every extrusion to divide the flow by it changed nothing. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 9947e40b8d..d8ea742b18 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8402,20 +8402,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio; _mm3_per_mm *= filament_flow_ratio; - // Belt printer: compensate for any volume change introduced by the mesh - // forward transform. path.mm3_per_mm is derived from slicer-frame layer - // height × line width, but a slicer-frame slab of volume V maps under the - // back-transform to a machine-frame region of volume V / |det(T)|. The - // mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)| - // is always 1 and this is currently a no-op; it is retained as a guard in - // case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame - // shear/scale acts on the g-code in BeltKinematics, not here.) - if (m_config.belt_printer.value) { - double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant()); - if (det > EPSILON) - _mm3_per_mm /= det; - } - if (path.role() == erTopSolidInfill) { _mm3_per_mm *= m_config.top_solid_infill_flow_ratio; } else if (path.role() == erBottomSurface) { From 0701f64ba90ed84e8dc7a53b76ae373ca1dbc685 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:44 -0500 Subject: [PATCH 49/75] Belt: index apron layers into the first layer's nozzle map Apron bands looked up their filament and nozzle config slot with a running counter, while object layers use Layer::id(), so band N read the map of object layer N. They precede layer 0 and now use its assignment. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 4 +++- src/libslic3r/GCode.hpp | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index d8ea742b18..df3b1205fb 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5326,7 +5326,9 @@ LayerResult GCode::process_belt_brim_layer( break; } - m_cur_layer_idx = m_belt_brim_layer_idx ++; + // Apron bands precede object layer 0 and have no layer id of their own; they take the + // filament and nozzle assignment in effect at the first object layer. + m_cur_layer_idx = 0; // Publish the band's Z for _extrude()'s first-layer-plane probe, and make sure // it cannot leak past this layer even if an extrusion throws. diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 5d99c219c6..3356219037 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -843,9 +843,6 @@ protected: // _extrude() needs for the first-layer-plane probe is published here instead. // Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set. std::optional m_belt_brim_z; - // Counter standing in for Layer::id() on apron layers, which precede layer 0. - size_t m_belt_brim_layer_idx{0}; - // Belt brim only. Brim and coincident apron bands are emitted before m_layer // is switched to their object, so belt_height_above_floor() would otherwise // read the previously visited object's belt description -- making a brim's From c0ba2c8c4447b4c361cf0e75b3610d1bba4472c7 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:44 -0500 Subject: [PATCH 50/75] Belt brim: read the belt axis from the config in the instance check belt_brim_instances_compatible() runs while the slicing parameters can be stale, like the rest of the brim predicates, which read the print config. Raised in Hanif Koh's review of #14394. --- src/libslic3r/PrintObject.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 6b9c5842bf..8100241ef8 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1249,7 +1249,9 @@ bool PrintObject::belt_brim_instances_compatible() const // matters for configurations that do not. if (m_instances.size() <= 1) return true; - const int axis = m_slicing_params.belt_floor_from_axis; + // From the config, not m_slicing_params: this runs while those can be stale. A tilt + // about Y runs the belt along X, any other tilt along Y (see compute_belt_height_and_floor). + const int axis = m_print->config().belt_slice_rotation.value == BeltRotationAxis::Y ? 0 : 1; const Point &ref = m_instances.front().shift; for (const PrintInstance &inst : m_instances) { const coord_t along = axis == 0 ? inst.shift.x() - ref.x() : inst.shift.y() - ref.y(); From f7b822abb74ca62e23e0ce062556624b6feac984 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:45 -0500 Subject: [PATCH 51/75] Belt: remove the unused clip_support_fills() It had no caller besides its own recursion. Raised in Hanif Koh's review of #14394. --- src/libslic3r/PrintObject.cpp | 61 ----------------------------------- 1 file changed, 61 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 8100241ef8..2c7cdc7335 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -4689,67 +4689,6 @@ void PrintObject::combine_infill() } } -// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly. -// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection. -static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region) -{ - ExtrusionEntitiesPtr new_entities; - for (ExtrusionEntity *entity : fills.entities) { - if (auto *path = dynamic_cast(entity)) { - ExtrusionEntityCollection clipped; - path->intersect_expolygons(clip_region, &clipped); - if (!clipped.empty()) { - for (ExtrusionEntity *e : clipped.entities) - new_entities.push_back(e->clone()); - } - delete entity; - } else if (auto *multipath = dynamic_cast(entity)) { - ExtrusionPaths new_paths; - for (const ExtrusionPath &p : multipath->paths) { - ExtrusionEntityCollection clipped; - p.intersect_expolygons(clip_region, &clipped); - for (ExtrusionEntity *e : clipped.entities) - if (auto *cp = dynamic_cast(e)) - new_paths.push_back(std::move(*cp)); - } - if (!new_paths.empty()) { - multipath->paths = std::move(new_paths); - new_entities.push_back(multipath); - } else { - delete entity; - } - } else if (auto *loop = dynamic_cast(entity)) { - ExtrusionPaths new_paths; - for (const ExtrusionPath &p : loop->paths) { - ExtrusionEntityCollection clipped; - p.intersect_expolygons(clip_region, &clipped); - for (ExtrusionEntity *e : clipped.entities) - if (auto *cp = dynamic_cast(e)) - new_paths.push_back(std::move(*cp)); - } - if (!new_paths.empty()) { - // Loop is no longer a closed loop after clipping; emit as individual paths. - for (auto &p : new_paths) - new_entities.push_back(new ExtrusionPath(std::move(p))); - delete entity; - } else { - delete entity; - } - } else if (auto *coll = dynamic_cast(entity)) { - clip_support_fills(*coll, clip_region); - if (!coll->empty()) { - new_entities.push_back(coll); - } else { - delete entity; - } - } else { - // Unknown entity type — keep as-is. - new_entities.push_back(entity); - } - } - fills.entities = std::move(new_entities); -} - void PrintObject::_generate_support_material() { if (is_tree(m_config.support_type.value)) { From 646fe6384c633afa41c05b3b9705178a893551e1 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:07:45 -0500 Subject: [PATCH 52/75] Belt: retire the two support floor modes nothing implements clip_only and both were never read and behaved like none; old values now load as none. Raised in Hanif Koh's review of #14394. --- src/libslic3r/PrintConfig.cpp | 5 +++-- src/libslic3r/PrintConfig.hpp | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cc17d0fdec..4e2458e73b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -373,8 +373,6 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis) static t_config_enum_values s_keys_map_BeltSupportFloorMode { { "none", int(BeltSupportFloorMode::None) }, { "generator_only", int(BeltSupportFloorMode::GeneratorOnly) }, - { "clip_only", int(BeltSupportFloorMode::ClipOnly) }, - { "both", int(BeltSupportFloorMode::Both) }, }; CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode) @@ -9354,6 +9352,9 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va //BBS: handle legacy options if (opt_key == "curr_bed_type" && value == "SuperTack Plate") { value = "Supertack Plate"; + } else if (opt_key == "belt_support_floor_mode" && (value == "clip_only" || value == "both")) { + // Never implemented; both behaved like "none". + value = "none"; } else if (opt_key == "enable_wipe_tower") { opt_key = "enable_prime_tower"; } else if (opt_key == "wipe_tower_width") { diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 9d20a71c9b..1a2dd0f7a3 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -275,8 +275,6 @@ enum class BeltSupportFloorMode { None, // No belt floor awareness GeneratorOnly, // Only in tree support drop_nodes/contact_points - ClipOnly, // Only post-processing clipping - Both, // Both generator and clipping }; enum class BeltSupportZOffsetMode From 556569c0e355f5eab5b6b87ed6a1fc8e8aba2064 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:10:57 -0500 Subject: [PATCH 53/75] Belt: drive the first-layer fan band from the generator, not from parsed moves The cooling buffer's band pass rebuilt positions from the layer's G-code and tested them against the first-layer plane. The G-code is in machine coordinates and the plane is in slicing coordinates, so on the shipped profiles the nearest move was over 100 mm from a 0.2 mm band and the pass never changed the fan. GCode::_extrude() already knows each path's height above the belt, so it now tags the band changes and the buffer applies and strips the tags. The pass also took the S of every M106 as the part fan, whatever its P index, and stored that 0..255 value where a percentage was expected (an auxiliary fan line came back as M106 S651); it now uses FanMover's parser, which ignores other fans, and converts to percent. It no longer overwrites the layer's intended speed, only the fan's actual state. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 8 + src/libslic3r/GCode.hpp | 5 + src/libslic3r/GCode/CoolingBuffer.cpp | 223 +++++++------------------- src/libslic3r/GCode/CoolingBuffer.hpp | 19 +-- src/libslic3r/GCode/FanMover.hpp | 5 + tests/fff_print/test_print.cpp | 55 +++++++ 6 files changed, 141 insertions(+), 174 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index df3b1205fb..151efbf6b2 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,6 +8507,14 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // the speed fade tracks perpendicular distance from the plane on // belt printers; otherwise this falls back to the slicing layer id. const int _layer = this->effective_layer_index_for_point(path_point_mm); + // Belt printers: tell the cooling buffer which band above the belt this path starts in, + // once per layer and at every change (CoolingBuffer::apply_belt_band_fan consumes it). + if (m_enable_cooling_markers && m_config.belt_printer.value && + (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != _layer)) { + gcode += ";_BELT_BAND:" + std::to_string(_layer) + "\n"; + m_belt_band_tag = _layer; + m_belt_band_tag_layer = m_layer_index; + } if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 3356219037..e390bc972f 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -2,6 +2,7 @@ #define slic3r_GCode_hpp_ #include "libslic3r.h" +#include #include "ExPolygon.hpp" #include "GCodeWriter.hpp" #include "GCode/BeltKinematics.hpp" @@ -855,6 +856,10 @@ protected: ~BeltFloorObjectGuard() { slot = nullptr; } }; + // Last ";_BELT_BAND" tag written and the layer it was written on (see _extrude()). + int m_belt_band_tag{std::numeric_limits::min()}; + int m_belt_band_tag_layer{std::numeric_limits::min()}; + std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; // BBS diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 1df6ce3bb9..29ab528de5 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,11 +1,13 @@ #include "../GCode.hpp" -#include "../FirstLayerPlane.hpp" #include "CoolingBuffer.hpp" +#include "FanMover.hpp" #include #include #include #include #include +#include +#include #include #include #include @@ -33,15 +35,12 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t m_extruder_ids.emplace_back(ex.id()); } - // Borrow the first-layer plane from the GCode generator. When inactive - // (non-belt printers and belt printers without Z shear), per-line fan - // re-evaluation is skipped and behavior is bit-identical to the legacy - // per-layer path. - m_first_layer_plane = gcodegen.first_layer_plane(); } void CoolingBuffer::reset(const Vec3d &position) { + m_belt_band_fan = -1; + m_belt_band_layer_fan = -1; // BBS: add I and J axis to store center of arc m_current_pos.assign(7, 0.f); m_current_pos[0] = float(position.x()); @@ -335,16 +334,13 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b if (flush) { // This is either an object layer or the very last print layer. Calculate cool down over the collected support layers // and one object layer. + const unsigned int extruder_at_start = m_current_extruder; std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); - // First-layer plane: per-segment fan re-evaluation post-pass. Walks - // the cooled-down gcode and inserts inline M106 commands at band - // crossings (where the path's perpendicular distance to the plane - // crosses close_fan_the_first_x_layers thresholds). No-op when - // the evaluator is inactive. - if (m_first_layer_plane && m_first_layer_plane->is_active()) - out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched); + // Belt printers: the fan follows each path's height above the belt (see apply_belt_band_fan). + if (out.find(";_BELT_BAND:") != std::string::npos) + out = this->apply_belt_band_fan(std::move(out), layer_time_stretched, extruder_at_start); m_gcode.clear(); } return out; @@ -1078,7 +1074,7 @@ std::string CoolingBuffer::apply_layer_cooldown( // Pure helper: compute the main fan speed for a given effective layer index. // Mirrors the inline logic in change_extruder_set_fan but is callable from -// per-line code in apply_first_layer_plane_fan_eval. +// per-tag code in apply_belt_band_fan. int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, unsigned int extruder_id) const { @@ -1116,173 +1112,74 @@ int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_ti return int(fan_speed_new); } -// Post-pass: walk the cooled-down gcode line by line, track XYZ position, -// and insert M106 commands at first-layer-plane band crossings so the fan -// follows perpendicular distance to the plane rather than the slicing-layer -// index. Only invoked when the FirstLayerPlane evaluator is active. +// Belt printers: a layer is a tilted slab, so "the first layer" is not a slicing layer but +// whatever lies within a layer height of the belt. GCode::_extrude() knows each path's +// height above the belt in the slicing frame and tags every change of it with +// ";_BELT_BAND:". This pass turns the tags into part-fan changes +// and strips them. (The G-code itself is in machine coordinates, which is why the band +// is not worked out from the moves here.) // -// This implementation is intentionally minimal: it overrides only the MAIN -// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc -// special fans remain at their layer-level values from apply_layer_cooldown. -// That keeps the per-line logic small while still giving the user precise -// fan control near the belt surface, which is the main quality concern. -std::string CoolingBuffer::apply_first_layer_plane_fan_eval( - std::string &&gcode_in, size_t /*layer_id*/, float layer_time) +// Only the main part-cooling fan is touched; overhang and bridge fans keep the values +// apply_layer_cooldown() gave them. +std::string CoolingBuffer::apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start) { - if (!m_first_layer_plane || !m_first_layer_plane->is_active()) - return std::move(gcode_in); - + static constexpr std::string_view band_tag = ";_BELT_BAND:"; const std::string &gcode = gcode_in; std::string out; - out.reserve(gcode.size() + 256); + out.reserve(gcode.size()); - // Match the PWM floor applied at every other set_fan call in this file so - // band-crossing M106 emissions start the fan reliably at low speeds. + // Match the PWM floor applied at every other set_fan call in this file. const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); + unsigned int active_extruder = extruder_at_start; - // Track position in slicing-frame mm. Seed from m_current_pos which the - // CoolingBuffer keeps up-to-date across layers. - Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]); - - // Track current main fan speed by parsing M106 commands as we walk so - // we can restore it after a band exit. - int current_main_fan = m_fan_speed; - int pre_band_main_fan = current_main_fan; - // Implicit initial state: assume the layer started "out of the band" - // (i.e., the layer-level fan setting from apply_layer_cooldown is in - // effect). The first movement we encounter will reconcile this. - bool in_first_layer_band = false; - unsigned int active_extruder = m_current_extruder; - - auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) { - if (line_sv.size() < 3) return false; - if (line_sv[0] != 'G') return false; - if (line_sv[1] != '0' && line_sv[1] != '1') return false; - if (line_sv[2] != ' ' && line_sv[2] != '\t') return false; - const char *c = line_sv.data() + 3; - const char *end = line_sv.data() + line_sv.size(); - bool any = false; - while (c < end && *c != ';') { - while (c < end && (*c == ' ' || *c == '\t')) ++c; - if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break; - char axis = *c; - ++c; - if (axis == 'X' || axis == 'Y' || axis == 'Z') { - char *next; - double v = std::strtod(c, &next); - if (next != c) { - if (axis == 'X') p.x() = v; - else if (axis == 'Y') p.y() = v; - else p.z() = v; - c = next; - any = true; - continue; - } - } - // Skip unrecognized word. - while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n') - ++c; - } - return any; - }; - - auto parse_m106 = [](const std::string_view &line_sv) -> int { - // Returns -1 if not an M106, otherwise the S value (0..255). - if (line_sv.size() < 4 || line_sv[0] != 'M') return -1; - if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6')) - return -1; - // Find S - size_t s_pos = line_sv.find('S'); - if (s_pos == std::string_view::npos) return -1; - const char *c = line_sv.data() + s_pos + 1; - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(std::clamp(v, 0, 255)); - }; - - auto parse_m107 = [](const std::string_view &line_sv) -> bool { - return line_sv.size() >= 4 && line_sv[0] == 'M' && - line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7'; - }; - - auto parse_tool_change = [this](const std::string_view &line_sv) -> int { - // Returns the new extruder id, or -1 if not a toolchange. - if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1; - if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0) - return -1; - const char *c = line_sv.data() + m_toolchange_prefix.size(); - char *next; - long v = std::strtol(c, &next, 10); - if (next == c) return -1; - return int(v); - }; - - const char *p = gcode.c_str(); - const char *end = gcode.c_str() + gcode.size(); + const char *p = gcode.data(); + const char *end = p + gcode.size(); while (p < end) { - const char *line_end = p; - while (line_end < end && *line_end != '\n') ++line_end; - const char *next_line = line_end; - if (next_line < end) ++next_line; // include the '\n' + const char *line_end = static_cast(std::memchr(p, '\n', end - p)); + if (line_end == nullptr) + line_end = end; + const char *next_line = line_end < end ? line_end + 1 : end; + const std::string_view line(p, line_end - p); - std::string_view line_sv(p, line_end - p); - - // Track tool changes so the per-line fan eval uses the right extruder. - int new_tool = parse_tool_change(line_sv); - if (new_tool >= 0) - active_extruder = unsigned(new_tool); - - // Track existing fan commands so we can restore the right value when - // exiting a band. - int m106_speed = parse_m106(line_sv); - if (m106_speed >= 0) { - current_main_fan = m106_speed; - if (!in_first_layer_band) - pre_band_main_fan = m106_speed; - } else if (parse_m107(line_sv)) { - current_main_fan = 0; - if (!in_first_layer_band) - pre_band_main_fan = 0; + if (line.size() > band_tag.size() && line.compare(0, band_tag.size(), band_tag) == 0) { + const int eff_idx = std::atoi(std::string(line.substr(band_tag.size())).c_str()); + const bool in_band = eff_idx < std::max(m_config.close_fan_the_first_x_layers.get_at(active_extruder), 1); + int target = this->compute_main_fan_speed(eff_idx, layer_time, active_extruder); + // Clear of the belt and nothing to add: fall back to what the layer asked for. + if (! in_band && target == 0 && m_belt_band_layer_fan >= 0) + target = m_belt_band_layer_fan; + if (target != m_belt_band_fan) { + out += GCodeWriter::set_fan(m_config.gcode_flavor, target, part_cooling_fan_min_pwm); + m_belt_band_fan = target; + } + // The tag itself is dropped. + p = next_line; + continue; } - // Movement line: parse XYZ, evaluate plane, possibly emit a fan - // change BEFORE this line. - bool moved = parse_xyz_into(line_sv, cur_pos_mm); - if (moved) { - const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm); - const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder); - const bool now_in_band = eff_idx < std::max(close_n, 1); - if (now_in_band != in_first_layer_band) { - // Band crossing: emit a M106 with the appropriate speed. - int target_fan; - if (now_in_band) { - // Entering the first-layer band: fan off. - pre_band_main_fan = current_main_fan; - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - } else { - // Exiting the band: restore the layer's normal fan speed. - // Use compute_main_fan_speed with the effective index so - // the linear ramp factor (close_fan→full_fan_speed_layer) - // also follows distance from the plane. - target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder); - if (target_fan == 0) - target_fan = pre_band_main_fan; - } - if (target_fan != current_main_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm); - current_main_fan = target_fan; - m_fan_speed = target_fan; - m_current_fan_speed = target_fan; - } - in_first_layer_band = now_in_band; + if (! line.empty() && line.front() == 'M') { + // Follow the fan commands of the layer-level cooling. FanMover's parser ignores + // auxiliary and chamber fans (M106 P2, P3...), which must not be taken for the part fan. + const int16_t raw = get_fan_speed(std::string(line), m_config.gcode_flavor); + if (raw >= 0) { + m_belt_band_fan = int(std::lround(double(std::min(raw, 255)) * 100. / 255.)); + m_belt_band_layer_fan = m_belt_band_fan; } + } else if (line.size() > m_toolchange_prefix.size() && line.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) == 0) { + char *num_end = nullptr; + const std::string num(line.substr(m_toolchange_prefix.size())); + const long tool = std::strtol(num.c_str(), &num_end, 10); + if (num_end != num.c_str() && tool >= 0) + active_extruder = unsigned(tool); } out.append(p, next_line - p); p = next_line; } + // The fan's real state, so the next layer's cooling re-issues its own speed if it differs. + if (m_belt_band_fan >= 0) + m_current_fan_speed = m_belt_band_fan; return out; } diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 1adb6f5005..0d758b5a5b 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -10,7 +10,6 @@ namespace Slic3r { class GCode; class Layer; -class FirstLayerPlane; struct PerExtruderAdjustments; // A standalone G-code filter, to control cooling of the print. @@ -37,13 +36,10 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - // First-layer plane: per-line fan re-evaluation post-pass. Walks the - // post-cooldown gcode, tracks XYZ position, and inserts M106 commands at - // band-crossing transitions in slicing-frame coordinates. Only runs - // when m_first_layer_plane is active. - std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in, - size_t layer_id, - float layer_time); + // Belt printers: turn the ";_BELT_BAND:" tags GCode::_extrude() leaves in the + // layer's G-code into part-fan changes, so the fan follows a path's height above the + // belt rather than the slicing layer index, and strip the tags. + std::string apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start); // Pure helper: compute the main fan speed for a given effective layer // index (layer-id units, mapped through the plane evaluator) and the @@ -74,9 +70,10 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // First-layer plane evaluator, borrowed from GCode. Null = inactive - // (legacy per-layer fan control). - const FirstLayerPlane *m_first_layer_plane = nullptr; + // Belt band pass state, kept across layers. The part fan as this pass last saw or set + // it (percent, -1 unknown), and the last value the layer-level cooling asked for. + int m_belt_band_fan = -1; + int m_belt_band_layer_fan = -1; }; } diff --git a/src/libslic3r/GCode/FanMover.hpp b/src/libslic3r/GCode/FanMover.hpp index 17addd855c..9567e8e2d5 100644 --- a/src/libslic3r/GCode/FanMover.hpp +++ b/src/libslic3r/GCode/FanMover.hpp @@ -27,6 +27,11 @@ public: } }; +// The part-cooling fan value a G-code line sets: the raw S (0..255) of an M106 that +// addresses the part fan, 0 for a fan-off command, -1 for any other line (auxiliary +// and chamber fans included). +int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor); + class FanMover { private: diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 6f0eb7b00e..6d8d3d2ec9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -596,3 +596,58 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P CHECK_FALSE(print.validate().string.empty()); } } + +// On a belt every tilted layer starts on the belt, so "first layer" cooling is a band along +// the belt, not the first slicing layers: the part fan goes off for the paths that start +// within a layer height of the belt and back on above it, on every layer. The G-code is in +// machine coordinates, so the generator tags the band changes and the cooling buffer +// applies them; before that the buffer compared machine-frame moves with a slicing-frame +// plane and never switched the fan at all. +TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print][belt][Cooling]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "close_fan_the_first_x_layers", 1 }, + { "full_fan_speed_layer", 0 }, + { "fan_min_speed", 100 }, + { "fan_max_speed", 100 }, + { "slow_down_layer_time", 1000 }, + { "fan_cooling_layer_time", 1001 }, + { "reduce_fan_stop_start_freq", 0 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string gcode = slice({ cube(20) }, config); + REQUIRE(! gcode.empty()); + + // The tags are consumed by the cooling buffer and never reach the file. + CHECK(gcode.find(";_BELT_BAND") == std::string::npos); + + size_t fan_off = 0, fan_on = 0; + GCodeReader parser; + parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { + if (line.cmd_is("M107")) + ++ fan_off; + else if (line.cmd_is("M106")) { + float s = 0.f; + if (line.has_value('S', s) && s <= 0.f) + ++ fan_off; + else + ++ fan_on; + } + }); + // A flat-bed print turns the fan on once. Here it cycles with the layers. + CHECK(fan_off > 10); + CHECK(fan_on > 10); +} From 360a68e07876b5bff9cd27d6df6c3cdcd93ce930 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:11:59 -0500 Subject: [PATCH 54/75] Belt: warn when the purge tower is enabled but the project has no tower object The purge tower is a model object the GUI creates and sizes, and libslic3r only purges into one that exists. A multi-filament belt project sliced from the CLI without it changed filament with nowhere to purge, silently. Raised in Hanif Koh's review of #14394. --- src/libslic3r/Print.cpp | 15 +++++++++++++++ tests/fff_print/test_print.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 5bfdfe8eac..444bf7cde6 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1553,6 +1553,21 @@ StringObjectException Print::validate(std::vector *warnin add_warning(warningtemp); } + // The purge tower is a model object the GUI creates and sizes; libslic3r only purges + // into one that exists. A project sliced without it (the CLI on a project saved before + // the tower was generated) changes filament with nowhere to purge. + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence != PrintSequence::ByObject + && ! m_config.spiral_mode.value && extruders.size() > 1 && ! this->has_belt_purge_tower()) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; " + "filament changes will not be purged. Open the project in the application " + "to generate the tower."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + add_warning(warningtemp); + } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) { const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { return object->config().belt_purge_tower_object.value; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 6d8d3d2ec9..f9c9528461 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -336,6 +336,36 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg CHECK_FALSE(print.has_wipe_tower()); } +// The GUI creates the purge tower object; a project sliced without one (the CLI) must say +// that its filament changes go unpurged. +TEST_CASE("Belt purge tower enabled without a tower object warns", "[Print][PurgeTower][belt]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 }, + { "layer_change_gcode", "G92 E0\n" } + }); + auto purge_warnings = [](Print &print) { + std::vector warnings; + print.validate(&warnings); + return std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) { + return w.opt_key == "enable_belt_purge_tower"; + }); + }; + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/2, /*overlap=*/false); + model.objects[1]->config.set_key_value("extruder", new ConfigOptionInt(2)); + print.apply(model, config); + REQUIRE(print.extruders().size() > 1); + CHECK(purge_warnings(print) == 1); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(purge_warnings(print) == 0); +} + TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]") { DynamicPrintConfig config = multifilament_config(2, { From 412564cae3e72caae3d626962e9065df99dd9621 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:11:59 -0500 Subject: [PATCH 55/75] Belt: document the frame of the exclude-object outlines EXCLUDE_OBJECT_DEFINE keeps plate coordinates on a belt printer: the frame after the slicing rotation is undone and before the G-code axis remap and machine-frame shear, which is where the object stands on the belt. Raised in Hanif Koh's review of #14394. --- src/libslic3r/GCode.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 151efbf6b2..360e35cae2 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -10289,6 +10289,10 @@ std::string GCode::set_object_info(Print *print) { for (PrintInstance& inst : object->instances()) { inst.unique_id = unique_id++; inst.id = inst_id++; + // Outlines are in plate coordinates. On a belt printer that is the frame after + // the slicing rotation has been undone and before the G-code axis remap and + // machine-frame shear: where the object stands on the belt, which is what an + // object picker shows. Klipper cancels by name, so nothing depends on more. auto bbox = inst.get_bounding_box(); auto center = print->translate_to_print_space(Vec2d(bbox.center().x(), bbox.center().y())); auto inst_name = get_instance_name(object, inst); From 3beb448ae6ffb082dfd685996784ac40d4672a19 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:13:29 -0500 Subject: [PATCH 56/75] Belt brim: lattice lines closer to the belt than the band fraction move uphill With a first layer of about 0.28 mm or more at 45 degrees (or a shallower belt) the brim band is wider than one bead and its lines go on the nominal lattice. A lattice line could land where the belt is almost at the band's print_z; its flow was clamped to half a layer while the nozzle sat nearly on the belt. Such a line now moves uphill to the 0.75 fraction the single-line case uses, and a line that lands on the previous one is skipped. Ported from the Unlayered fork (patch 0007 of its belt port series, found there by fuzzing first layer heights). The fork's companion fix, restricting the brim filament to those the writer was handed (0008), is not needed here: ToolOrdering registers the brim filament on every band's layer, so the writer always has it. A test pins that with every object a flush target. --- src/libslic3r/BeltBrim.cpp | 40 ++++++++++++++------ tests/fff_print/test_skirt_brim.cpp | 58 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index fa119c6609..92877ef0aa 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -1,3 +1,4 @@ +#include #include "BeltBrim.hpp" #include "ClipperUtils.hpp" @@ -284,7 +285,33 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // must not be pooled before the flow is resolved. // Overshoot the region so the clip, not the line's ends, decides the extent. const coord_t margin = coord_t(SCALED_EPSILON) + 1; - for (const coord_t u : us) { + coord_t u_prev = std::numeric_limits::min(); + for (coord_t u : us) { + // Nozzle-to-belt clearance for this line. Constant along the line, because the + // belt height depends only on the shear-axis coordinate. Band-anchored lines + // share one clearance by construction; lattice lines (shallow belts, or a first + // layer thick enough that the band is wider than a bead) each get their own. + // + // A lattice line can fall where the belt is only a hair below the band's print_z. + // The bead there would be laid scraping the belt while its flow is sized for a + // taller cell, so it is moved uphill to the same fraction of the band the + // single-line case uses. (The clearance is along slice Z; the real gap under the + // nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.) + double clearance = uniform_clearance; + if (clearance <= 0.) { + const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u); + clearance = print_z - bc.ctx.floor_print_z(probe); + if (clearance < BAND_CLEARANCE_FRACTION * height) { + clearance = BAND_CLEARANCE_FRACTION * height; + u = scale_(bc.ctx.cutoff_u(print_z - clearance)); + } + clearance = std::min(clearance, height); + } + // Two lattice lines moved to the same place are one line. + if (u == u_prev) + continue; + u_prev = u; + Polyline line; if (bc.frame.from_axis == 0) line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)), @@ -299,17 +326,6 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, if (pieces.empty()) continue; - // Nozzle-to-belt clearance for this line. Constant along the line, because the - // belt height depends only on the shear-axis coordinate. Band-anchored lines - // share one clearance by construction; lattice lines (shallow belts) each get - // their own, clamped so neither end of a band yields an unprintable bead. - double clearance = uniform_clearance; - if (clearance <= 0.) { - const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u); - clearance = print_z - bc.ctx.floor_print_z(probe); - clearance = std::min(std::max(clearance, 0.5 * height), height); - } - // with_cross_section, not with_height: it reaches the prescribed volume while // KEEPING the extrusion spacing, so the bead is sized to fill exactly one // pitch x clearance cell of the sheet. diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 5fa810106f..edd2f0a56a 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include "test_helpers.hpp" // get access to init_print, etc @@ -1195,3 +1196,60 @@ TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]") REQUIRE(! gc.empty()); CHECK(role_passes(gc, "brim") > 0); } + +// With a 0.3 mm first layer at 45 degrees the brim band on the belt is wider than one bead, +// so its lines go on the nominal lattice instead of at a fixed fraction of the band. A +// lattice line can then land where the belt is almost at the band's print_z; it must be +// moved uphill to the same 0.75 fraction the single-line case uses, not laid scraping the +// belt with its flow clamped to half a layer. +TEST_CASE("Belt brim lattice lines keep their clearance above the belt", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "layer_height", 0.3 }, + { "initial_layer_print_height", 0.3 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + }); + const std::string gcode = slice({ cube(20) }, config); + + // Heights of the brim extrusions, from the ;HEIGHT: tags inside ;TYPE:Brim sections. + std::vector brim_heights; + bool in_brim = false; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";TYPE:")) + in_brim = boost::starts_with(line, ";TYPE:Brim"); + else if (in_brim && boost::starts_with(line, ";HEIGHT:")) + brim_heights.push_back(std::stod(line.substr(8))); + } + REQUIRE(! brim_heights.empty()); + for (const double h : brim_heights) { + CHECK(h >= 0.75 * 0.3 - 1e-3); + CHECK(h <= 0.3 + 1e-3); + } +} + +// The brim prints in the object's outer wall filament even when every extrusion of the object +// is offered to purging (flush_into_objects): the tool ordering registers the brim filament +// itself, so the writer always knows it. +TEST_CASE("Belt brim slices when every object is a flush target", "[SkirtBrim][belt]") +{ + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "flush_into_objects", 1 }, + { "flush_into_infill", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + REQUIRE(print.validate().string.empty()); + const std::string out = gcode(print); + CHECK(out.find(";TYPE:Brim") != std::string::npos); +} From 1b392955de708bef61851650a347aa465fcb9fb9 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 01:13:29 -0500 Subject: [PATCH 57/75] tests: organic tree supports reaching the belt slice without a negative flow Covers the case from Hanif Koh's review of #14394 (belt raft layers below the object with no lower bound), which the negative-Z bottom layer fix in layer_initialize() addresses. --- tests/fff_print/test_print.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index f9c9528461..5b3baf7dc5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -681,3 +681,32 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] CHECK(fan_off > 10); CHECK(fan_on > 10); } + +// Organic supports under an overhang on a belt printer reach below the object's first layer, +// where the virtual belt raft layers sit at negative Z. The lowest of them used to get a +// negative height and abort slicing with a negative flow error. +TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Print][belt][Support]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "belt_printer", 1 }, + { "belt_slice_rotation", "x" }, + { "belt_slice_rotation_angle", 45 }, + { "belt_slice_rotation_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "z_hop", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + std::string gcode; + REQUIRE_NOTHROW(gcode = slice({ TestMesh::overhang }, config)); + CHECK(! gcode.empty()); +} From 461063856d5184fc0e927c405bb61aff55e7a519 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 02:33:14 -0500 Subject: [PATCH 58/75] tests: fix two belt test expectations The clearance test needs the relative-E reset in its layer change G-code to get past validate()'s other checks, and now asserts the height message. The fan band test counts cycles rather than commands: the band is decided per path start, so a cube cycles the fan far less often than a benchy. --- tests/fff_print/test_print.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 5b3baf7dc5..b07c8c986a 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -609,6 +609,7 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P { "gcode_remap_z", "pos_y" }, { "printable_height", printable_height }, { "skirt_loops", 0 }, + { "layer_change_gcode", "G92 E0\n" }, }); return config; }; @@ -623,7 +624,7 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P Print print; Model model; init_print({ cube(60) }, print, model, belt_config(50)); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.find("height") != std::string::npos); } } @@ -664,22 +665,21 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] // The tags are consumed by the cooling buffer and never reach the file. CHECK(gcode.find(";_BELT_BAND") == std::string::npos); - size_t fan_off = 0, fan_on = 0; + // The fan commands in order: '0' off, '1' on. + std::string fan_sequence; GCodeReader parser; parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { if (line.cmd_is("M107")) - ++ fan_off; + fan_sequence += '0'; else if (line.cmd_is("M106")) { float s = 0.f; - if (line.has_value('S', s) && s <= 0.f) - ++ fan_off; - else - ++ fan_on; + fan_sequence += (line.has_value('S', s) && s <= 0.f) ? '0' : '1'; } }); - // A flat-bed print turns the fan on once. Here it cycles with the layers. - CHECK(fan_off > 10); - CHECK(fan_on > 10); + // A flat-bed print turns the fan on once and leaves it on. Here it goes off again for + // paths that start back down at the belt, and on again above it. + INFO("fan sequence: " << fan_sequence); + CHECK(fan_sequence.find("101") != std::string::npos); } // Organic supports under an overhang on a belt printer reach below the object's first layer, From 441ae8e877378105b907824abe321d1256ae6119 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 02:36:13 -0500 Subject: [PATCH 59/75] Belt: decide the fan band per extrusion segment A tilted layer runs from the belt to the top of the part, so a wall loop that starts above the belt still passes along it. Tagging only the path's first point left such loops out of the band entirely; the band is now evaluated at each segment, with the tag capped where the fan stops depending on it. --- src/libslic3r/GCode.cpp | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 360e35cae2..45910cae00 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,14 +8507,25 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // the speed fade tracks perpendicular distance from the plane on // belt printers; otherwise this falls back to the slicing layer id. const int _layer = this->effective_layer_index_for_point(path_point_mm); - // Belt printers: tell the cooling buffer which band above the belt this path starts in, - // once per layer and at every change (CoolingBuffer::apply_belt_band_fan consumes it). - if (m_enable_cooling_markers && m_config.belt_printer.value && - (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != _layer)) { - gcode += ";_BELT_BAND:" + std::to_string(_layer) + "\n"; - m_belt_band_tag = _layer; - m_belt_band_tag_layer = m_layer_index; - } + // Belt printers: tell the cooling buffer which band above the belt the next extrusion is + // in (CoolingBuffer::apply_belt_band_fan consumes the tag). A tilted layer runs from the + // belt to the top of the part, so this is decided per segment, not per path; a tag is + // written once per layer and at every change, capped where the fan stops depending on it. + const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; + const int belt_band_cap = belt_band_tags + ? std::max(m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()), + m_config.full_fan_speed_layer.get_at(m_writer->filament()->id())) + 1 : 0; + auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_cap, z = path_point_mm.z()](coord_t x, coord_t y) { + if (! belt_band_tags) + return; + const int band = std::min(this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)), belt_band_cap); + if (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != band) { + gcode += ";_BELT_BAND:" + std::to_string(band) + "\n"; + m_belt_band_tag = band; + m_belt_band_tag_layer = m_layer_index; + } + }; + tag_belt_band(path.first_point().x(), path.first_point().y()); if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached @@ -8999,6 +9010,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length); } } + tag_belt_band((line.a.x() + line.b.x()) / 2, (line.a.y() + line.b.y()) / 2); if (path.z_contoured) { // ZAA: Z anti-aliased extrusion with variable Z per point Vec2d dest2d = this->point_to_gcode(line.b.to_point()); @@ -9129,6 +9141,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, const ProcessedPoint &processed_point = new_points[i]; const ProcessedPoint &pre_processed_point = new_points[i-1]; Vec3d p = this->point_to_gcode_quantized(processed_point.p); + tag_belt_band((pre_processed_point.p.x() + processed_point.p.x()) / 2, (pre_processed_point.p.y() + processed_point.p.y()) / 2); if (m_enable_cooling_markers) { if (enable_overhang_bridge_fan) { cur_fan_enabled = check_overhang_fan(processed_point.overlap, path.role()); From 31494a9836aa417892c1096ffa3a292325a1f6fc Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 02:42:14 -0500 Subject: [PATCH 60/75] tests: give the belt fan band test a band the walls reach --- tests/fff_print/test_print.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index b07c8c986a..8cdd9e3c33 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -649,7 +649,9 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, { "z_hop", 0 }, - { "close_fan_the_first_x_layers", 1 }, + // Three layers: the lowest wall of each tilted layer is centred about 0.3 mm above + // the belt (half a line width in from the contact edge), outside a one-layer band. + { "close_fan_the_first_x_layers", 3 }, { "full_fan_speed_layer", 0 }, { "fan_min_speed", 100 }, { "fan_max_speed", 100 }, From bfbf5ad1c28b4f2a3cadfac802dd814f907263d3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:09:59 -0500 Subject: [PATCH 61/75] Arrange: keep a pile aligned to an off-centre point on the bed With best_object_pos away from the bed centre the placer packs the pile inside the bin and then translates it so its centre lands on that point, without checking that it still fits there. A belt printer aims at the leading end of the belt (BabyBelt Pro: 0.5, 0.05), so any pile longer than the 50 mm around that point was pushed past the edge: four 90 mm parts on the 95 x 500 mm belt ended with one across the edge and one outside while 290 mm of belt stayed free. The final alignment now stops the pile at the edge of the bin; the items' inflated boxes leave the object spacing as the margin. A pile that does not fit along an axis is centred on it, as before. --- .../include/libnest2d/placers/nfpplacer.hpp | 22 +++++++++++++++- tests/libslic3r/test_arrange.cpp | 26 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp index dc7733ebd6..cf66c4c97e 100644 --- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp +++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp @@ -1111,7 +1111,27 @@ private: default: ; // DONT_ALIGN } - auto d = cb - ci; + auto d = cb - ci; + + // Keep the pile on the bin. A target near an edge (a belt printer starts its parts + // at the leading end of the belt) would otherwise centre a pile that is larger than + // the room around that point on it and push part of the pile off the bed. The pile + // stops at the edge instead; the items' boxes carry their inflation, which is the + // margin left there. A pile that does not fit along an axis is centred on it. + { + auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) { + if (hi - lo >= bin_hi - bin_lo) + return (bin_lo + bin_hi) / 2 - (lo + hi) / 2; + if (lo + shift < bin_lo) + shift = bin_lo - lo; + if (hi + shift > bin_hi) + shift = bin_hi - hi; + return shift; + }; + setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d))); + setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d))); + cb = ci + d; + } // BBS make sure the item won't clash with excluded regions // do we have wipe tower after arranging? diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index 3906cba8ba..a8bc56d2d5 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -1,3 +1,4 @@ +#include #include #include "libslic3r/Arrange.hpp" @@ -259,6 +260,31 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") require_no_overlap(items); } +// A belt printer starts its parts at the leading end of the belt (best_object_pos 0.5, 0.05). +// Centring a pile on a point that close to the edge pushed everything longer than the room +// around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the +// edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge. +TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") +{ + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + + arrange(items, belt, params); + + coord_t lowest = std::numeric_limits::max(); + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); + CHECK(belt.contains(bb)); + lowest = std::min(lowest, bb.min.y()); + } + // Snapped to the edge it was aimed at, less the spacing margin, not re-centred. + CHECK(lowest < scaled(10.)); + require_no_overlap(items); +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers From 7a23d9ea2caaef06afdee45514921cf214a8c058 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:43 -0500 Subject: [PATCH 62/75] Belt slicing: only model parts set the layer range again The filter was lost with the debug logging it shared an #if with, so modifiers and support blockers stretched the sliced range of a belt object. --- src/libslic3r/BeltSliceStrategy.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index d16819e7db..74d8566000 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -41,6 +41,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, double min_z = std::numeric_limits::max(); for (const ModelVolume *mv : model_volumes) { + if (!mv->is_model_part()) continue; Transform3d vol_trafo = trafo * mv->get_matrix(); const auto &its = mv->mesh().its; for (const stl_vertex &v : its.vertices) { From 007071bd56988198dacc3e5cf4441b3cd32590a0 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 63/75] GCode: skip the second crossing-avoidance pass on a brim apron layer too travel_to runs avoid-crossing a second time after a wipe; that call needs the current Layer as much as the first one does. --- src/libslic3r/GCode.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 45910cae00..4227bf7817 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -9461,7 +9461,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string // When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters. // Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction() // FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations. - if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall) { + if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall + && m_layer != nullptr) { // A brim apron layer has no Layer to avoid crossing // If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call. if (used_external_mp_once) m_avoid_crossing_perimeters.use_external_mp_once(); From 997fac87526e0ae2e12e9cebed41fa1e164f26be Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 64/75] GCodeProcessor: key the belt height-check skip on belt_printer The check was skipped for any machine-frame transform; what makes the printable height meaningless is the belt axis, so ask for that directly. --- src/libslic3r/GCode/GCodeProcessor.cpp | 20 +++++++++++--------- src/libslic3r/GCode/GCodeProcessor.hpp | 1 + 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index c2aedd8262..bf5d5ea980 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -2777,9 +2777,9 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int // Belt-printer post-gcode shear/scale/post_remap is applied as the final // step of BeltKinematics::to_machine, so MoveVertex.position is - // in the printer's machine frame. Undo it here so XY area and Z height - // checks operate in the build-volume frame that printable_area / - // printable_height are defined in. For non-belt printers + // in the printer's machine frame. Undo it here so the XY area check + // operates in the build-volume frame that printable_area is defined in + // (the height checks below are skipped on belt printers). For non-belt printers // (is_active() == false) apply_inverse is identity and behaviour is // unchanged from before. const bool machine_frame_active = m_machine_frame_transform.is_active(); @@ -2860,11 +2860,12 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int valid = false; } } - // Belt printers: machine Z is belt travel, which grows without bound over a - // print, while printable_height is the clearance above the belt; the two are - // not comparable, so the over-height check is skipped, as the preview's - // ToolHeightOutside warning already is. - if ( !machine_frame_active && iter->second.max_print_z > plate_printable_height ) { //over height + // Belt printers: the Z recorded here grows with belt travel (machine Z with the + // frame transform, the slicing-frame Z without it), while printable_height is the + // clearance above the belt; the two are not comparable, so the over-height check + // is skipped, as the preview's ToolHeightOutside warning already is. + // Print::validate() checks the object's height against the clearance. + if ( !m_belt_printer && iter->second.max_print_z > plate_printable_height ) { //over height m_result.gcode_check_result.error_code |= (1 << 3); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -2905,7 +2906,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int } // check printable height - if (!machine_frame_active && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { + if (!m_belt_printer && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) { m_result.gcode_check_result.error_code |= (1 << 1); std::pair filament_to_object_id; filament_to_object_id.first = iter->first; @@ -3076,6 +3077,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config) // bounds rather than machine-frame positions. m_machine_frame_transform.init_from_config(config); m_result.machine_frame_transform_active = m_machine_frame_transform.is_active(); + m_belt_printer = config.belt_printer.value; auto filament_maps = config.option("filament_map"); if (filament_maps != nullptr) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 85cc5a7544..6090a55eb6 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1164,6 +1164,7 @@ class Print; // transform on move positions so bounds checks operate in the // pre-machine-frame (build-volume) frame. MachineFrameTransform m_machine_frame_transform; + bool m_belt_printer{ false }; unsigned int m_line_id; unsigned int m_last_line_id; From 550229b0faa79b93934679b70dff2c36a93e1f10 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 65/75] Belt validation: refuse a brim next to a purge tower object The purge plan moves objects onto a common layer grid after the brim bands are built, so the two cannot share a print. The prime tower setting alone still does not block a brim. The missing-prism warning now counts the filaments the objects use, as the GUI does. --- src/libslic3r/GCode/ToolOrdering.cpp | 4 ++-- src/libslic3r/Print.cpp | 12 ++++++++---- tests/fff_print/test_skirt_brim.cpp | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index a59e5aa46b..a9f56a805f 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1066,8 +1066,8 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_ // The `print_z < object_bottom_z` clause reads "below the object" as "raft // gap". On a belt printer that is wrong: the brim apron legitimately prints // below the object's first layer, and treating those layers as raft would put a - // wipe tower at negative Z. Belt brim and the prime tower are mutually - // exclusive (rejected in Print::validate()), so simply drop the clause there. + // wipe tower at negative Z. A belt printer never prints the classic + // prime tower (Print::has_wipe_tower()), so simply drop the clause there. // // Gate on config.belt_printer, NOT on has_belt_brim: every layer below the // object bottom on a belt printer is legitimately a sub-object stream - brim diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 444bf7cde6..66de6d88ee 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1420,10 +1420,14 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // spiral vase cannot share. The prime tower setting is no obstacle: belt - // printers never print the classic tower, and the belt purge prism is an - // ordinary object that never takes a brim. + // neither the belt purge tower nor spiral vase can share (the purge plan moves + // objects onto a common layer grid after the brim bands are built). The prime + // tower setting alone is no obstacle: it stays on for any multi-filament + // project, and belt printers never print the classic tower. if (this->has_belt_brim()) { + if (this->has_belt_purge_tower()) + return { L("Brim is not compatible with the belt purge tower. " + "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; @@ -1558,7 +1562,7 @@ StringObjectException Print::validate(std::vector *warnin // the tower was generated) changes filament with nowhere to purge. if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value && m_config.print_sequence != PrintSequence::ByObject - && ! m_config.spiral_mode.value && extruders.size() > 1 && ! this->has_belt_purge_tower()) { + && ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) { StringObjectException warningtemp; warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; " "filament changes will not be purged. Open the project in the application " diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index edd2f0a56a..d762c49421 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -930,6 +930,28 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK(print.validate().string.empty()); CHECK_FALSE(gcode(print).empty()); } + // An actual purge tower object is different: the purge plan moves objects onto a common + // layer grid after the brim bands are built, so the two are still refused together. + SECTION("a brim is refused next to a belt purge tower object") { + DynamicPrintConfig config = belt_brim_multifilament_config(2, { + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "enable_belt_purge_tower", 1 }, + }); + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + Print print; + Model model; + init_print({ cube(20), cube(20) }, print, model, config, &overrides); + CHECK(print.validate().string.empty()); + + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + CHECK_FALSE(print.validate().string.empty()); + } } TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]") From 5ac0dd19d04d90fc25e3d50d51f15febe04f0ca5 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 66/75] Belt brim: no near-duplicate lines in a narrow lattice band A band barely wider than one line got a second line almost on top of the first, and lattice rows could repeat within half a pitch. --- src/libslic3r/BeltBrim.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 92877ef0aa..45ef5d14c9 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -253,7 +253,10 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, std::vector us; double uniform_clearance = 0.; // 0 => derive per line from its own position double line_pitch = bc.in_plane_pitch; - if (band_in_plane <= bc.in_plane_pitch + EPSILON) { + // One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm + // first layer at 45 degrees): its flow is matched to the band, so the bead is that much + // wider. Two lattice lines in such a band would land almost on top of each other. + if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) { // Steep belt, which is the normal case: the band is narrower than one bead, so // exactly one line fits. Place it at a FIXED fraction of the band rather than // on a nominal-spacing lattice. On a lattice each line lands at an arbitrary @@ -307,8 +310,9 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, } clearance = std::min(clearance, height); } - // Two lattice lines moved to the same place are one line. - if (u == u_prev) + // A line moved uphill can land on, or almost on, its neighbour; two beads closer + // than half a pitch would be laid into the same cell. + if (u_prev != std::numeric_limits::min() && std::abs(u - u_prev) < bc.pitch_u / 2) continue; u_prev = u; From d35af1806a8d6d918c2b24059504f2580502ab3d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Fri, 2 Oct 2026 12:46:44 -0500 Subject: [PATCH 67/75] Belt cooling: keep the fan off in the band inside the layer cooling pass The band was a second pass over the finished layer that fought the fan commands the layer pass had already written (overhang, bridge and resume requests). The generator now marks where each segment enters and leaves the band and the layer pass treats the band as the strongest fan request, so there is one place that decides the fan. --- src/libslic3r/GCode.cpp | 25 ++--- src/libslic3r/GCode.hpp | 5 +- src/libslic3r/GCode/CoolingBuffer.cpp | 151 +++++--------------------- src/libslic3r/GCode/CoolingBuffer.hpp | 19 +--- src/libslic3r/GCode/FanMover.hpp | 5 - tests/fff_print/test_print.cpp | 75 +++++++++---- 6 files changed, 95 insertions(+), 185 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4227bf7817..f98cf62fc4 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -8507,25 +8507,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description, // the speed fade tracks perpendicular distance from the plane on // belt printers; otherwise this falls back to the slicing layer id. const int _layer = this->effective_layer_index_for_point(path_point_mm); - // Belt printers: tell the cooling buffer which band above the belt the next extrusion is - // in (CoolingBuffer::apply_belt_band_fan consumes the tag). A tilted layer runs from the - // belt to the top of the part, so this is decided per segment, not per path; a tag is - // written once per layer and at every change, capped where the fan stops depending on it. - const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; - const int belt_band_cap = belt_band_tags - ? std::max(m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()), - m_config.full_fan_speed_layer.get_at(m_writer->filament()->id())) + 1 : 0; - auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_cap, z = path_point_mm.z()](coord_t x, coord_t y) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so the + // "first layers" the fan stays off for are a band along the belt. Mark where the + // extrusion enters and leaves it, per segment, for the cooling buffer. + const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value; + const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()) : 0; + auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) { if (! belt_band_tags) return; - const int band = std::min(this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)), belt_band_cap); - if (m_belt_band_tag_layer != m_layer_index || m_belt_band_tag != band) { - gcode += ";_BELT_BAND:" + std::to_string(band) + "\n"; - m_belt_band_tag = band; - m_belt_band_tag_layer = m_layer_index; + const bool in_band = this->effective_layer_index_for_point(Vec3d(unscale(x), unscale(y), z)) < belt_band_layers; + if (in_band != m_belt_in_band) { + gcode += in_band ? ";_BELT_BAND_START\n" : ";_BELT_BAND_END\n"; + m_belt_in_band = in_band; } }; - tag_belt_band(path.first_point().x(), path.first_point().y()); if (path_on_first_layer || object_layer_over_raft()) { //BBS: for solid infill of first layer, speed can be higher as long as //wall lines have be attached diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index e390bc972f..17221101fd 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -856,9 +856,8 @@ protected: ~BeltFloorObjectGuard() { slot = nullptr; } }; - // Last ";_BELT_BAND" tag written and the layer it was written on (see _extrude()). - int m_belt_band_tag{std::numeric_limits::min()}; - int m_belt_band_tag_layer{std::numeric_limits::min()}; + // The last extrusion segment was inside the belt's first-layer fan band (see _extrude()). + bool m_belt_in_band{false}; std::set m_initial_layer_extruders; std::vector> m_sorted_layer_filaments; diff --git a/src/libslic3r/GCode/CoolingBuffer.cpp b/src/libslic3r/GCode/CoolingBuffer.cpp index 29ab528de5..a47dd2c148 100644 --- a/src/libslic3r/GCode/CoolingBuffer.cpp +++ b/src/libslic3r/GCode/CoolingBuffer.cpp @@ -1,13 +1,10 @@ #include "../GCode.hpp" #include "CoolingBuffer.hpp" -#include "FanMover.hpp" #include #include #include #include #include -#include -#include #include #include #include @@ -39,8 +36,7 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t void CoolingBuffer::reset(const Vec3d &position) { - m_belt_band_fan = -1; - m_belt_band_layer_fan = -1; + m_belt_band_active = false; // BBS: add I and J axis to store center of arc m_current_pos.assign(7, 0.f); m_current_pos[0] = float(position.x()); @@ -80,6 +76,9 @@ struct CoolingLine // ORCA: Add support for ironing fan speed control TYPE_IRONING_FAN_START = 1 << 19, TYPE_IRONING_FAN_END = 1 << 20, + // Belt printers: extrusions within the first-layer band above the belt. + TYPE_BELT_BAND_START = 1 << 21, + TYPE_BELT_BAND_END = 1 << 22, }; CoolingLine(unsigned int type, size_t line_start, size_t line_end) : @@ -334,13 +333,9 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b if (flush) { // This is either an object layer or the very last print layer. Calculate cool down over the collected support layers // and one object layer. - const unsigned int extruder_at_start = m_current_extruder; std::vector per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos); float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments); out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments); - // Belt printers: the fan follows each path's height above the belt (see apply_belt_band_fan). - if (out.find(";_BELT_BAND:") != std::string::npos) - out = this->apply_belt_band_fan(std::move(out), layer_time_stretched, extruder_at_start); m_gcode.clear(); } return out; @@ -544,6 +539,10 @@ std::vector CoolingBuffer::parse_layer_gcode(const std:: line.type = CoolingLine::TYPE_IRONING_FAN_START; } else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control line.type = CoolingLine::TYPE_IRONING_FAN_END; + } else if (boost::starts_with(sline, ";_BELT_BAND_START")) { + line.type = CoolingLine::TYPE_BELT_BAND_START; + } else if (boost::starts_with(sline, ";_BELT_BAND_END")) { + line.type = CoolingLine::TYPE_BELT_BAND_END; } else if (boost::starts_with(sline, "G4 ")) { // Parse the wait time. line.type = CoolingLine::TYPE_G4; @@ -885,7 +884,9 @@ std::string CoolingBuffer::apply_layer_cooldown( {CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false}, {CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control {CoolingLine::TYPE_FORCE_RESUME_FAN, false}}; - bool need_set_fan = false; + // Belt printers: a band still open from the previous layer has to take the fan back from + // the layer-level speed issued just above. + bool need_set_fan = m_belt_band_active; for (const CoolingLine *line : lines) { const char *line_start = gcode.c_str() + line->line_start; @@ -899,6 +900,8 @@ std::string CoolingBuffer::apply_layer_cooldown( if (new_extruder != m_current_extruder) { m_current_extruder = new_extruder; change_extruder_set_fan(true); + if (m_belt_band_active) + need_set_fan = true; } } new_gcode.append(line_start, line_end - line_start); @@ -951,6 +954,13 @@ std::string CoolingBuffer::apply_layer_cooldown( if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value) new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed); } + else if (line->type & CoolingLine::TYPE_BELT_BAND_START) { + m_belt_band_active = true; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_BELT_BAND_END) { + m_belt_band_active = false; + need_set_fan = true; + } else if (line->type & CoolingLine::TYPE_EXTRUDE_END) { // Just remove this comment. } else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) { @@ -1043,7 +1053,15 @@ std::string CoolingBuffer::apply_layer_cooldown( m_current_fan_speed = speed; } }; - if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ + if (m_belt_band_active) { + // Belt printers: a tilted layer runs from the belt to the top of the part, so + // "the first layers" are a band along the belt rather than the first slicing + // layers. Extrusions GCode::_extrude() marks as inside that band print with the + // fan off, whatever overhang, bridge or resume request is pending, as the first + // layers of a flat bed do. Leaving the band falls through to the branches below. + set_fan(0); + fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false; + } else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){ set_fan(overhang_fan_speed); } else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control set_fan(internal_bridge_fan_speed); @@ -1072,115 +1090,4 @@ std::string CoolingBuffer::apply_layer_cooldown( return new_gcode; } -// Pure helper: compute the main fan speed for a given effective layer index. -// Mirrors the inline logic in change_extruder_set_fan but is callable from -// per-tag code in apply_belt_band_fan. -int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const -{ -#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id) - float fan_min_speed = EXTRUDER_CFG(fan_min_speed); - float fan_max_speed = EXTRUDER_CFG(fan_max_speed); - bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq); - int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers); - int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer); - float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time)); - float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time)); -#undef EXTRUDER_CFG - - if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0) - close_fan_the_first_x_layers = 1; - - float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f; - if (effective_layer_id >= close_fan_the_first_x_layers) { - if (layer_time < slow_down_layer_time) { - fan_speed_new = fan_max_speed; - } else if (layer_time < fan_cooling_layer_time) { - double t = (layer_time - slow_down_layer_time) / - (fan_cooling_layer_time - slow_down_layer_time); - fan_speed_new = float(int(floor(t * fan_min_speed + - (1. - t) * fan_max_speed) + 0.5)); - } - if (effective_layer_id + 1 < full_fan_speed_layer) { - float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers) - / float(full_fan_speed_layer - close_fan_the_first_x_layers); - fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255)); - } - } else { - fan_speed_new = 0.f; - } - return int(fan_speed_new); -} - -// Belt printers: a layer is a tilted slab, so "the first layer" is not a slicing layer but -// whatever lies within a layer height of the belt. GCode::_extrude() knows each path's -// height above the belt in the slicing frame and tags every change of it with -// ";_BELT_BAND:". This pass turns the tags into part-fan changes -// and strips them. (The G-code itself is in machine coordinates, which is why the band -// is not worked out from the moves here.) -// -// Only the main part-cooling fan is touched; overhang and bridge fans keep the values -// apply_layer_cooldown() gave them. -std::string CoolingBuffer::apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start) -{ - static constexpr std::string_view band_tag = ";_BELT_BAND:"; - const std::string &gcode = gcode_in; - std::string out; - out.reserve(gcode.size()); - - // Match the PWM floor applied at every other set_fan call in this file. - const unsigned int part_cooling_fan_min_pwm = static_cast(std::max(0, m_config.part_cooling_fan_min_pwm.value)); - unsigned int active_extruder = extruder_at_start; - - const char *p = gcode.data(); - const char *end = p + gcode.size(); - while (p < end) { - const char *line_end = static_cast(std::memchr(p, '\n', end - p)); - if (line_end == nullptr) - line_end = end; - const char *next_line = line_end < end ? line_end + 1 : end; - const std::string_view line(p, line_end - p); - - if (line.size() > band_tag.size() && line.compare(0, band_tag.size(), band_tag) == 0) { - const int eff_idx = std::atoi(std::string(line.substr(band_tag.size())).c_str()); - const bool in_band = eff_idx < std::max(m_config.close_fan_the_first_x_layers.get_at(active_extruder), 1); - int target = this->compute_main_fan_speed(eff_idx, layer_time, active_extruder); - // Clear of the belt and nothing to add: fall back to what the layer asked for. - if (! in_band && target == 0 && m_belt_band_layer_fan >= 0) - target = m_belt_band_layer_fan; - if (target != m_belt_band_fan) { - out += GCodeWriter::set_fan(m_config.gcode_flavor, target, part_cooling_fan_min_pwm); - m_belt_band_fan = target; - } - // The tag itself is dropped. - p = next_line; - continue; - } - - if (! line.empty() && line.front() == 'M') { - // Follow the fan commands of the layer-level cooling. FanMover's parser ignores - // auxiliary and chamber fans (M106 P2, P3...), which must not be taken for the part fan. - const int16_t raw = get_fan_speed(std::string(line), m_config.gcode_flavor); - if (raw >= 0) { - m_belt_band_fan = int(std::lround(double(std::min(raw, 255)) * 100. / 255.)); - m_belt_band_layer_fan = m_belt_band_fan; - } - } else if (line.size() > m_toolchange_prefix.size() && line.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) == 0) { - char *num_end = nullptr; - const std::string num(line.substr(m_toolchange_prefix.size())); - const long tool = std::strtol(num.c_str(), &num_end, 10); - if (num_end != num.c_str() && tool >= 0) - active_extruder = unsigned(tool); - } - - out.append(p, next_line - p); - p = next_line; - } - - // The fan's real state, so the next layer's cooling re-issues its own speed if it differs. - if (m_belt_band_fan >= 0) - m_current_fan_speed = m_belt_band_fan; - return out; -} - } // namespace Slic3r diff --git a/src/libslic3r/GCode/CoolingBuffer.hpp b/src/libslic3r/GCode/CoolingBuffer.hpp index 0d758b5a5b..e0b5044921 100644 --- a/src/libslic3r/GCode/CoolingBuffer.hpp +++ b/src/libslic3r/GCode/CoolingBuffer.hpp @@ -36,18 +36,6 @@ private: // Returns the adjusted G-code. std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector &per_extruder_adjustments); - // Belt printers: turn the ";_BELT_BAND:" tags GCode::_extrude() leaves in the - // layer's G-code into part-fan changes, so the fan follows a path's height above the - // belt rather than the slicing layer index, and strip the tags. - std::string apply_belt_band_fan(std::string &&gcode_in, float layer_time, unsigned int extruder_at_start); - - // Pure helper: compute the main fan speed for a given effective layer - // index (layer-id units, mapped through the plane evaluator) and the - // current extruder. Mirrors the inline logic in the change_extruder_set_fan - // lambda but is callable from per-line code. - int compute_main_fan_speed(int effective_layer_id, float layer_time, - unsigned int extruder_id) const; - // G-code snippet cached for the support layers preceding an object layer. std::string m_gcode; // Internal data. @@ -70,10 +58,9 @@ private: unsigned int m_current_nozzle; //BBS: current fan speed int m_current_fan_speed; - // Belt band pass state, kept across layers. The part fan as this pass last saw or set - // it (percent, -1 unknown), and the last value the layer-level cooling asked for. - int m_belt_band_fan = -1; - int m_belt_band_layer_fan = -1; + // Belt printers: the extrusion being processed lies in the first-layer band above the + // belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers. + bool m_belt_band_active = false; }; } diff --git a/src/libslic3r/GCode/FanMover.hpp b/src/libslic3r/GCode/FanMover.hpp index 9567e8e2d5..17addd855c 100644 --- a/src/libslic3r/GCode/FanMover.hpp +++ b/src/libslic3r/GCode/FanMover.hpp @@ -27,11 +27,6 @@ public: } }; -// The part-cooling fan value a G-code line sets: the raw S (0..255) of an M106 that -// addresses the part fan, 0 for a fan-off command, -1 for any other line (auxiliary -// and chamber fans included). -int16_t get_fan_speed(const std::string &line, GCodeFlavor flavor); - class FanMover { private: diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8cdd9e3c33..e9a98324d9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -20,6 +20,9 @@ #include "test_utils.hpp" #include +#include +#include +#include #include #include #include @@ -628,13 +631,11 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P } } -// On a belt every tilted layer starts on the belt, so "first layer" cooling is a band along -// the belt, not the first slicing layers: the part fan goes off for the paths that start -// within a layer height of the belt and back on above it, on every layer. The G-code is in -// machine coordinates, so the generator tags the band changes and the cooling buffer -// applies them; before that the buffer compared machine-frame moves with a slicing-frame -// plane and never switched the fan at all. -TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print][belt][Cooling]") +// On a belt every tilted layer starts on the belt, so "the first layers" the fan stays off +// for are a band along the belt, not the first slicing layers. The generator marks where +// each extrusion segment enters and leaves that band and the cooling buffer keeps the fan +// off inside it, on every layer. +TEST_CASE("Belt printers keep the part fan off within the band above the belt", "[Print][belt][Cooling]") { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ @@ -649,8 +650,8 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] { "initial_layer_print_height", 0.2 }, { "skirt_loops", 0 }, { "z_hop", 0 }, - // Three layers: the lowest wall of each tilted layer is centred about 0.3 mm above - // the belt (half a line width in from the contact edge), outside a one-layer band. + // Three layers, 0.6 mm: the lowest wall of each tilted layer is centred about 0.3 mm + // above the belt (half a line width in from the contact edge). { "close_fan_the_first_x_layers", 3 }, { "full_fan_speed_layer", 0 }, { "fan_min_speed", 100 }, @@ -664,24 +665,50 @@ TEST_CASE("Belt printers switch the part fan by height above the belt", "[Print] const std::string gcode = slice({ cube(20) }, config); REQUIRE(! gcode.empty()); - // The tags are consumed by the cooling buffer and never reach the file. + // The markers are consumed by the cooling buffer and never reach the file. CHECK(gcode.find(";_BELT_BAND") == std::string::npos); - // The fan commands in order: '0' off, '1' on. - std::string fan_sequence; - GCodeReader parser; - parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) { - if (line.cmd_is("M107")) - fan_sequence += '0'; - else if (line.cmd_is("M106")) { - float s = 0.f; - fan_sequence += (line.has_value('S', s) && s <= 0.f) ? '0' : '1'; + // With this axis mapping machine Y is the height above the belt along the gantry. Walk + // the moves with the fan state: extrusions that stay within 0.45 mm of the belt are well + // inside the band and must print with the fan off; extrusions that stay 5 mm clear of it + // must print with it on. The first three slicing layers have the fan off altogether. + size_t in_band = 0, in_band_fan_on = 0, clear = 0, clear_fan_off = 0; + int layer = -1; + bool fan_on = false; + double y = 0.; + std::istringstream lines(gcode); + for (std::string line; std::getline(lines, line); ) { + if (boost::starts_with(line, ";LAYER_CHANGE")) { + ++ layer; + } else if (boost::starts_with(line, "M107")) { + fan_on = false; + } else if (boost::starts_with(line, "M106")) { + const size_t s = line.find('S'); + fan_on = s != std::string::npos && std::atof(line.c_str() + s + 1) > 0.; + } else if (boost::starts_with(line, "G1 ")) { + const size_t comment = line.find(';'); + const std::string cmd = line.substr(0, comment); + const size_t ypos = cmd.find(" Y"), epos = cmd.find(" E"); + if (ypos == std::string::npos) + continue; + const double y_new = std::atof(cmd.c_str() + ypos + 2); + const bool extruding = epos != std::string::npos && std::atof(cmd.c_str() + epos + 2) > 0.; + if (extruding && layer >= 3) { + if (std::max(y, y_new) < 0.45) { + ++ in_band; + in_band_fan_on += fan_on; + } else if (std::min(y, y_new) > 5.) { + ++ clear; + clear_fan_off += ! fan_on; + } + } + y = y_new; } - }); - // A flat-bed print turns the fan on once and leaves it on. Here it goes off again for - // paths that start back down at the belt, and on again above it. - INFO("fan sequence: " << fan_sequence); - CHECK(fan_sequence.find("101") != std::string::npos); + } + CHECK(in_band > 20); + CHECK(in_band_fan_on == 0); + CHECK(clear > 20); + CHECK(clear_fan_off == 0); } // Organic supports under an overhang on a belt printer reach below the object's first layer, From 488c6d8226a55699f488df4a4457aaf8c6a83d0d Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 10:02:36 -0500 Subject: [PATCH 68/75] Belt profiles: bump the IdeaFormer and Printcepts bundle versions The IR3 V2 and BabyBelt Pro printer profiles changed since the last bump; the updater only installs a strictly newer bundle. --- resources/profiles/IdeaFormer.json | 2 +- resources/profiles/Printcepts.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/profiles/IdeaFormer.json b/resources/profiles/IdeaFormer.json index f9f32d2f3f..ee076dfffb 100644 --- a/resources/profiles/IdeaFormer.json +++ b/resources/profiles/IdeaFormer.json @@ -1,6 +1,6 @@ { "name": "IdeaFormer", - "version": "02.00.00.04", + "version": "02.00.00.05", "force_update": "0", "description": "IdeaFormer belt printer configurations", "machine_model_list": [ diff --git a/resources/profiles/Printcepts.json b/resources/profiles/Printcepts.json index 1abe84873b..cdeda8f04d 100644 --- a/resources/profiles/Printcepts.json +++ b/resources/profiles/Printcepts.json @@ -1,6 +1,6 @@ { "name": "Printcepts", - "version": "01.00.00.02", + "version": "01.00.00.03", "force_update": "0", "description": "Printcepts belt printer configurations", "machine_model_list": [ From 6b304884d24ded41e428517e995decf5915dca47 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 12:03:18 -0500 Subject: [PATCH 69/75] Belt arrange: group colours along the belt, keep the purge tower's strip free, stop the tower at the plate end On a belt the parts print in belt order, so every colour change between parts is a filament change. Arrange packs items in extruder order already, but it grew the pile around its centre, so the colours ended up interleaved. A belt print now packs from the leading end of the bed, each row filling across the belt before the pile advances, and the objective charges an item for every packed part of another colour it does not fully follow along the belt, counting the tilted layers that reach cot(angle) * height past a part, so each colour prints as one run. The direction follows the slicing rotation: a rotation about X prints toward +Y, one about Y toward -X, and a negative angle flips it. Packing from the edge also means the brim has to be kept on the bed: a belt brim is printed brim_width wide for every brim type, so that much is reserved along every edge (between parts the brims may overlap, as on any printer). The purge prism is regenerated from the arranged parts, flush with the far edge of the bed, yet arrange moved it about like a part and packed parts into the strip it comes back to. Arrange now skips the prism and reserves its strip with a fixed virtual item, like a bed exclusion area, whenever the parts use more than one filament. The prism's length follows the parts plus a ramp per unit of height; with the height at its cap that ran 100 mm past the end of a 500 mm belt and the project could not print. The bar now stops at the plate end, and the purge planner's existing warning reports what the shortened bar cannot absorb. --- src/libslic3r/Arrange.cpp | 93 +++++++++++++++++++++++++--- src/libslic3r/Arrange.hpp | 7 +++ src/slic3r/GUI/BeltPurgeTower.cpp | 34 ++++++++--- src/slic3r/GUI/BeltPurgeTower.hpp | 4 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 97 ++++++++++++++++++++++++++++-- src/slic3r/GUI/Jobs/ArrangeJob.hpp | 1 + tests/libslic3r/test_arrange.cpp | 44 ++++++++++++++ 7 files changed, 254 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index fd496537f4..d6e386b5d4 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -276,7 +276,13 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam template void fill_config(PConf& pcfg, const ArrangeParams ¶ms) { - if (params.is_seq_print) { + if (params.is_belt) { + // Pack from the end of the belt that prints first. + pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT : + params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT : + PConf::Alignment::BOTTOM_RIGHT; + } + else if (params.is_seq_print) { // Start placing the items from the center of the print bed pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT; } @@ -421,7 +427,51 @@ protected: return bindist; } - double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) + // Belt printers pack from the end of the belt that prints first, and a corner + // packer's checks (pile inside the bin, pack origin) apply to them as well. + bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; } + + static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); } + + // Position along the belt in print order: increasing from the end that prints first. + double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); } + double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); } + double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); } + + // The corner of the bin the belt pile grows from. + Box::PointType belt_origin() const + { + const Box bb = sl::boundingBox(m_bin); + auto o = bb.minCorner(); + if (params.belt_reversed) { + if (params.belt_axis == 0) setX(o, getX(bb.maxCorner())); + else setY(o, getY(bb.maxCorner())); + } + return o; + } + + // An item's far edge in print order is what it costs (so a row fills across the + // belt before the pile advances), with a slight pull toward the near lateral + // edge and the same penalty as the bottom-left heuristic for sitting outside + // the corner. + double dist_along_belt(const Box &ibb) + { + const Box bin = sl::boundingBox(m_bin); + const int l = 1 - params.belt_axis; + const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l); + double d = belt_end(ibb) - belt_start(bin); + d += lat < 0 ? 10 * -lat : 0.1 * lat; + if (double behind = belt_start(ibb) - belt_start(bin); behind < 0) + d += 10 * -behind; + return norm(d); + } + + double corner_bindist(const Box &ibb, const ClipperLib::IntPoint &origin_pack) + { + return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack); + } + + double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment) { double bindist = 0; if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT) @@ -510,8 +560,8 @@ protected: // The smalles distance from the arranged pile center: double dist = norm(*(std::min_element(dists.begin(), dists.end()))); - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) { - double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) { + double bindist = corner_bindist(ibb, origin_pack); score = 0.2 * dist + 0.8 * bindist; } else { @@ -568,8 +618,8 @@ protected: break; } case LAST_BIG_ITEM: { - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) { - score = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) { + score = corner_bindist(ibb, origin_pack); } else { if (m_pilebb.defined) @@ -584,8 +634,8 @@ protected: // already processed bigger items. // No need to play around with the anchor points, the center will be // just fine for small items - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) - score = dist_for_BOTTOM_LEFT(ibb, origin_pack); + if (corner_packing()) + score = corner_bindist(ibb, origin_pack); else { // Align mainly around existing items score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack)); @@ -686,6 +736,28 @@ protected: score += 1 * (new_extruder_cnt-last_extruder_cnt); } + // On a belt the parts print in belt order, so every colour change between + // parts is a filament change. Items arrive sorted by extruder and the pile + // grows from the leading end; keep each colour's run contiguous by charging + // an item for every packed item of another colour it does not fully follow, + // counting the tilted layers that reach belt_tilt_slope * height past that + // item's far edge. + if (params.is_belt && !params.is_seq_print) { + const std::set item_colours(item.extrude_ids.begin(), item.extrude_ids.end()); + const double item_start = belt_start(ibb); + for (Item &p : m_items) { + if (p.is_virt_object) + continue; + const std::set p_colours(p.extrude_ids.begin(), p.extrude_ids.end()); + const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end()) + || std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end()); + if (same_colour) + continue; + if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope)) + score += 10.; + } + } + return std::make_tuple(score, fullbb); } @@ -762,7 +834,8 @@ public: auto binbb = sl::boundingBox(m_bin); - auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center(); + auto starting_point = this->params.is_belt ? belt_origin() : + cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center(); // if we have wipe tower, items should be arranged around wipe tower for (Item itm : items) { if (itm.is_wipe_tower) { @@ -913,7 +986,7 @@ std::function AutoArranger::g auto mp = m_merged_pile; mp.emplace_back(itm.transformedShape()); auto chull = sl::convexHull(mp); - if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) + if (corner_packing()) { if (!sl::isInside(chull, m_bin)) score += LARGE_COST_TO_REJECT; diff --git a/src/libslic3r/Arrange.hpp b/src/libslic3r/Arrange.hpp index ad125aee7d..b8fc0f7268 100644 --- a/src/libslic3r/Arrange.hpp +++ b/src/libslic3r/Arrange.hpp @@ -137,6 +137,13 @@ struct ArrangeParams { float nozzle_height = 0; float printable_height = 256.0; Vec2d align_center{ 0.5,0.5 }; + // Belt printer: items print in the order they lie along the belt axis, from + // its low end unless belt_reversed, and a part's top prints + // belt_tilt_slope * height further along it than its base. + bool is_belt = false; + int belt_axis = 1; // 0 = X, 1 = Y + bool belt_reversed = false; + float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted ArrangePolygons excluded_regions; // regions cant't be used ArrangePolygons nonprefered_regions; // regions can be used but not prefered diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index a8b0b0c0b8..3dc50d4cb2 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -266,6 +266,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const Vec3d plate_origin = plate->get_origin(); new_sig.key[10] = q(plate_origin.x()); new_sig.key[11] = q(plate_origin.y()); + if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed = get_extents(bed_opt->values); + new_sig.key[12] = q(bed.max.x()); + new_sig.key[13] = q(bed.max.y()); + } if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model @@ -278,26 +283,35 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and // belt_end >= y_max + z_max*cot (parts' top features print further up the // belt). The trailing z_max*cot term dominates the bar's own ramp. + // The bed (printable_area) is plate-local but model instances live in the + // plate's world frame, so the plate origin is added to every bed coordinate. + const double inset = 1.; + BoundingBoxf bed_ext; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) + bed_ext = get_extents(bed_opt->values); + + // Along the belt the bar stops at the end of the plate: a longer bar cannot be + // printed, and the cross-sections it loses there are reported by the purge + // planner when the parts' last layers then purge more than the bar holds. const double margin = 5.; const double ramp_compensation = height / sin_t; const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; - const double belt_start = std::max(belt_origin, belt_min - ramp_compensation); // leading ramp, toward belt origin - const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + if (bed_ext.defined) + belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); + const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin const double length = std::max(belt_end - belt_start, 10.); + belt_end = belt_start + length; const double belt_center = 0.5 * (belt_start + belt_end); // Across-belt: flush against the bed's maximum edge, inset by half the bar // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed. The bed (printable_area) is plate-local but model instances - // live in the plate's world frame, so add the plate origin's lateral - // component. lat_min/lat_max come from instance_bounding_box (world frame). + // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - const double inset = 1.; double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - if (const auto *bed_opt = printer_config.option("printable_area"); - bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + if (bed_ext.defined) { + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; } diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index c5a6d301a0..27d5b47c94 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -22,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[12] = {0}; // rounded geometry and plate inputs (0.1 mm units) + long key[14] = {0}; // rounded geometry, plate and bed inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 12; ++i) + for (int i = 0; i < 14; ++i) if (key[i] != o.key[i]) return false; return true; diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index cd4cc83dab..a21bf52c19 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -15,6 +15,9 @@ #include "slic3r/GUI/GUI_ObjectList.hpp" #include "libnest2d/common.hpp" +#include "libslic3r/Geometry.hpp" +#include +#include #define SAVE_ARRANGE_POLY 0 @@ -63,6 +66,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) { @@ -77,6 +88,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("printable_area")->values); + const bool belt_is_y = params.belt_axis == 1; + std::vector regions; + + std::set 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("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(); @@ -128,6 +196,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 inst_sel(mo->instances.size(), false); @@ -176,6 +246,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 @@ -206,6 +277,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]; @@ -252,6 +325,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& extruder_ids) @@ -290,10 +364,9 @@ 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, which is a real model object and arranges like any other. - if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); - belt_opt && belt_opt->value) + // 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; @@ -399,6 +472,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); @@ -434,6 +509,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 @@ -785,6 +861,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) { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.hpp b/src/slic3r/GUI/Jobs/ArrangeJob.hpp index 0c9f03de01..19143c3254 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.hpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.hpp @@ -46,6 +46,7 @@ class ArrangeJob : public Job //BBS:prepare the items from current selected partplate void prepare_partplate(); void prepare_wipe_tower(); + void prepare_belt_regions(int num_plates); ArrangePolygon prepare_arrange_polygon(void* instance); diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index a8bc56d2d5..1c13180f01 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -285,6 +285,50 @@ TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") require_no_overlap(items); } +// On a belt the parts print in belt order, so two colours that alternate along the +// belt, or sit side by side, cost a filament change on every shared layer. Arrange +// keeps each colour together: no part shares belt length with a part of another +// colour, counting the tilted layers that run cot(angle) * height past its far edge, +// whichever end of the belt prints first. +TEST_CASE("Arrange groups the colours of a belt print along the belt", "[Arrange][belt]") +{ + const bool reversed = GENERATE(false, true); + CAPTURE(reversed); + const BoundingBox belt = bed(95, 500); + ArrangePolygons items = squares(6, 30., 20.); + for (size_t i = 0; i < items.size(); ++i) + items[i].extrude_ids = { int(i % 3) + 1 }; // three colours, two parts each + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + params.is_belt = true; + params.belt_axis = 1; + params.belt_reversed = reversed; + params.belt_tilt_slope = 1.f; // 45 degrees + + arrange(items, belt, params); + require_no_overlap(items); + + // Belt position in print order, so the same check serves both directions. + const coord_t dir = reversed ? -1 : 1; + auto start = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.max.y() : bb.min.y()); }; + auto end = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.min.y() : bb.max.y()) + scaled(ap.height * params.belt_tilt_slope); }; + + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + CHECK(belt.contains(ap.transformed_poly().contour.bounding_box())); + } + for (const ArrangePolygon &a : items) + for (const ArrangePolygon &b : items) { + if (a.extrude_ids == b.extrude_ids) + continue; + // The part printed later starts after the earlier one has finished. + const coord_t earlier_end = start(a) <= start(b) ? end(a) : end(b); + const coord_t later_start = std::max(start(a), start(b)); + INFO("colour " << a.extrude_ids.front() << " vs " << b.extrude_ids.front()); + CHECK(earlier_end <= later_start); + } +} + TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]") { // The only place sequential-print clearance is enforced. The arrange menu offers From 0c3b7bc72e50541ef99794aff654edb1e61d0703 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 12:03:18 -0500 Subject: [PATCH 70/75] Belt brim: move the apron bands with the purge plan's layer grid The purge plan snaps every object onto one layer grid after the brim is built; the per-layer brim bands follow their layers but the apron bands below the first layer carry their own print_z and were left behind, which is why a brim was refused next to a purge tower object. The shift now moves them too and the combination is accepted again. --- src/libslic3r/BeltPurge.cpp | 3 +++ src/libslic3r/Print.cpp | 10 +++------- tests/fff_print/test_skirt_brim.cpp | 11 +++++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index cff3de8846..c4cbc83425 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -322,6 +322,9 @@ void PrintObject::belt_shift_layer_grid(double delta) layer->print_z += delta; for (SupportLayer *layer : m_support_layers) layer->print_z += delta; + // The brim's apron bands below the first layer carry their own print_z. + for (BeltBrimBand &band : m_belt_brim_prologue) + band.print_z += delta; m_slicing_params.belt_floor_z_shift += delta; } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 66de6d88ee..550cfbbd64 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1420,14 +1420,10 @@ StringObjectException Print::validate(std::vector *warnin return { L("Draft shield is not compatible with belt printer mode.") }; // Belt brim spans many layers and owns the layers below the object, which - // neither the belt purge tower nor spiral vase can share (the purge plan moves - // objects onto a common layer grid after the brim bands are built). The prime - // tower setting alone is no obstacle: it stays on for any multi-filament - // project, and belt printers never print the classic tower. + // spiral vase cannot share. The prime tower setting is no obstacle: belt + // printers never print the classic tower, and the belt purge prism is an + // ordinary object that never takes a brim. if (this->has_belt_brim()) { - if (this->has_belt_purge_tower()) - return { L("Brim is not compatible with the belt purge tower. " - "Disable one of them.") }; if (m_config.spiral_mode.value) return { L("Brim is not compatible with spiral vase mode on a belt printer. " "Disable one of them.") }; diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index d762c49421..ede355c151 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -930,9 +930,9 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira CHECK(print.validate().string.empty()); CHECK_FALSE(gcode(print).empty()); } - // An actual purge tower object is different: the purge plan moves objects onto a common - // layer grid after the brim bands are built, so the two are still refused together. - SECTION("a brim is refused next to a belt purge tower object") { + // A purge tower object is accepted too: the purge plan moves every object, apron + // bands included, onto one layer grid. + SECTION("a brim is accepted next to a belt purge tower object") { DynamicPrintConfig config = belt_brim_multifilament_config(2, { { "brim_type", "outer_only" }, { "brim_width", 4 }, @@ -945,12 +945,11 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira Print print; Model model; init_print({ cube(20), cube(20) }, print, model, config, &overrides); - CHECK(print.validate().string.empty()); - model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); print.apply(model, config); REQUIRE(print.has_belt_purge_tower()); - CHECK_FALSE(print.validate().string.empty()); + CHECK(print.validate().string.empty()); + CHECK_FALSE(gcode(print).empty()); } } From 2782374d8d287eeea24a6badb59ab863fb69bf15 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 13:12:41 -0500 Subject: [PATCH 71/75] Belt: write the belt header outside the optional file header block The G-code viewer takes the belt tilt only from the belt header comments, but they were written inside the header block that is left out when a BTT TFT thumbnail is configured, so such a printer never got belt view. The comments are not part of the header block; they go after it, and after the thumbnails that firmware needs first. --- src/libslic3r/GCode.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f98cf62fc4..d2fbea5742 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3093,8 +3093,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato file.write_format("; HEADER_BLOCK_START\n"); // Write information on the generator. file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str()); - // Belt printer: embed angle and transform configs in header via virtual hook. - this->write_belt_header(file, print); if (is_bbl_printers) file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str()); //BBS: total layer number @@ -3193,6 +3191,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato } } + // Belt printer: the tilt and transform settings the G-code viewer reads back. They + // are comments outside the config block, so they go after the thumbnails that a + // BTT TFT firmware needs first, and are written whether or not that header block is. + this->write_belt_header(file, print); // Write some terse information on the slicing parameters. const PrintObject *first_object = print.objects().front(); From 82c462ab2aa4cd5a61979f248d94b966ccb91ecd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 13:12:41 -0500 Subject: [PATCH 72/75] Prepare view: drop the dormant tilted-bed rendering Plater::set_bed_shape read the belt keys from the plater's own config, which never carries them, so the branch that tilted the bed model, drew the slicing arrow and plane and switched the build volume to belt mode never ran. The Prepare view shows the bed as the slicing pipeline treats it, flat; the gravity arrow from build_plate_tilt stays, as does the preview's belt view, which takes its angle from the G-code header. --- src/libslic3r/BuildVolume.cpp | 30 -------- src/libslic3r/BuildVolume.hpp | 8 -- src/slic3r/GUI/3DBed.cpp | 129 +-------------------------------- src/slic3r/GUI/3DBed.hpp | 18 ----- src/slic3r/GUI/GCodeViewer.cpp | 6 +- src/slic3r/GUI/Plater.cpp | 31 -------- 6 files changed, 3 insertions(+), 219 deletions(-) diff --git a/src/libslic3r/BuildVolume.cpp b/src/libslic3r/BuildVolume.cpp index 4efcb89a91..15e6668bae 100644 --- a/src/libslic3r/BuildVolume.cpp +++ b/src/libslic3r/BuildVolume.cpp @@ -180,31 +180,6 @@ BuildVolume::BuildVolume(const std::vector &printable_area, const double BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name(); } -void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y) -{ - m_is_belt_printer = enabled; - m_belt_angle = angle_deg; - m_belt_infinite_y = infinite_y; - - // Restart from the unmodified bbox each call. Without this, toggling - // belt mode off (or switching infinite_y true→false) would leave the - // extents inflated and break collision / object_state checks. - BoundingBoxf bboxf = get_extents(m_bed_shape); - m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) }; - - if (enabled) { - if (infinite_y) { - // Extend the Y bound to a very large value for infinite belt. - m_bboxf.max.y() = 100000.; - } - // Belt printer: the Z extent already equals printable_height (set above), which - // is the usable vertical clearance above the belt. The gantry's axis range is - // sized to reach height/cos(tilt), so no diagonal scaling is applied here — this - // keeps the live "outside build volume" highlight in agreement with Print::validate(). - (void) angle_deg; - } -} - #if 0 // Tests intersections of projected triangles, not just their vertices against a bounding box. // This test also correctly evaluates collision of a non-convex object with the bounding box. @@ -413,11 +388,6 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i build_volume.max.z() = std::numeric_limits::max(); if (ignore_bottom) build_volume.min.z() = -std::numeric_limits::max(); - // Belt printer: extend Y bounds for infinite Y. - if (m_is_belt_printer && m_belt_infinite_y) { - build_volume.min.y() = -std::numeric_limits::max(); - build_volume.max.y() = std::numeric_limits::max(); - } BoundingBox3Base build_volumef(build_volume.min.cast(), build_volume.max.cast()); // The following test correctly interprets intersection of a non-convex object with a rectangular build volume. //return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z()); diff --git a/src/libslic3r/BuildVolume.hpp b/src/libslic3r/BuildVolume.hpp index 2b0d8c2746..f311d026b0 100644 --- a/src/libslic3r/BuildVolume.hpp +++ b/src/libslic3r/BuildVolume.hpp @@ -57,10 +57,6 @@ public: // Initialize from PrintConfig::printable_area and PrintConfig::printable_height BuildVolume(const std::vector &printable_area, const double printable_height, const std::vector> &extruder_areas, const std::vector& extruder_printable_heights); - // Belt printer configuration. - void set_belt_printer(bool enabled, double angle_deg, bool infinite_y); - bool is_belt_printer() const { return m_is_belt_printer; } - // Source data, unscaled coordinates. const std::vector& printable_area() const { return m_bed_shape; } double printable_height() const { return m_max_print_height; } @@ -143,10 +139,6 @@ private: // Source definition of the print volume height (PrintConfig::printable_height) double m_max_print_height { 0.f }; std::vector m_extruder_printable_height; - // Belt printer state. - bool m_is_belt_printer { false }; - double m_belt_angle { 0. }; - bool m_belt_infinite_y { false }; // Derived values. BuildVolume_Type m_type { BuildVolume_Type::Invalid }; diff --git a/src/slic3r/GUI/3DBed.cpp b/src/slic3r/GUI/3DBed.cpp index 3032059678..69e5a1d340 100644 --- a/src/slic3r/GUI/3DBed.cpp +++ b/src/slic3r/GUI/3DBed.cpp @@ -387,9 +387,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR); - // Belt printer: bed rotation is applied inside render_model() and render_default() - // using m_is_belt_printer and m_belt_angle members. - switch (m_type) { case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; } @@ -398,8 +395,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix, } render_gravity_arrow(view_matrix, projection_matrix); - render_slicing_arrow(view_matrix, projection_matrix); - render_slicing_plane(view_matrix, projection_matrix); glsafe(::glDisable(GL_DEPTH_TEST)); } @@ -700,12 +695,6 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj shader->start_using(); shader->set_uniform("emission_factor", 0.0f); Transform3d model_matrix = Geometry::assemble_transform(m_model_offset); - // Belt printer: rotate the bed model about the tilt axis so the belt tilt - // is visible. Negative angle: belt surface tilts downward away from the nozzle. - if (m_is_belt_printer && m_belt_angle > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - model_matrix = Eigen::AngleAxisd(-angle_rad, belt_tilt_unit_axis()) * model_matrix; - } shader->set_uniform("volume_world_matrix", model_matrix); shader->set_uniform("view_model_matrix", view_matrix * model_matrix); shader->set_uniform("projection_matrix", projection_matrix); @@ -797,114 +786,6 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform shader->stop_using(); } -void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix) -{ - if (!m_is_belt_printer || m_belt_angle <= 0.f) - return; - - // Build the arrow model: shorter and wider than the gravity arrow. - if (!m_slicing_arrow.is_initialized()) { - const float stem_length = 15.0f; // shorter than gravity arrow (25) - const float stem_radius = 1.0f; // wider than gravity arrow (~0.33) - const float tip_radius = 3.0f; // wider tip - const float tip_length = 5.0f; - m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length)); - } - - // The slicing direction: layers stack along the gantry normal, i.e. the image of - // +Z under the mesh rotation about the tilt axis. Use the same AngleAxis as the - // slicing pipeline so the arrow matches whichever tilt axis is configured. - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Vec3d slice_dir = (Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()).toRotationMatrix() - * Vec3d::UnitZ()).normalized(); - - // Compute rotation to align +Z (arrow default) with slice_dir. - Vec3d from = Vec3d::UnitZ(); - double dot = from.dot(slice_dir); - Transform3d rot = Transform3d::Identity(); - if (dot < -0.9999) { - rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot; - } else if (dot < 0.9999) { - Vec3d axis = from.cross(slice_dir).normalized(); - double angle = std::acos(std::clamp(dot, -1.0, 1.0)); - rot = Eigen::AngleAxisd(angle, axis) * rot; - } - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - // Disable depth test so the arrow is always visible (not occluded by the tilted bed). - glsafe(::glDisable(GL_DEPTH_TEST)); - shader->start_using(); - - const Camera& camera = wxGetApp().plater()->get_camera(); - Transform3d model_matrix = rot; - shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix); - shader->set_uniform("projection_matrix", camera.get_projection_matrix()); - - m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink - m_slicing_arrow.render(); - - shader->stop_using(); - glsafe(::glEnable(GL_DEPTH_TEST)); -} - -void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix) -{ - if (!m_is_belt_printer || m_belt_angle <= 0.f) - return; - - // Build a quad in the XZ plane (world frame) representing the belt slicing plane. - // The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)). - // We render it as a semi-transparent quad centered on the build plate. - if (!m_slicing_plane.is_initialized()) { - const float half_size = 120.f; // mm, large enough to be visible - GLModel::Geometry init_data; - init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 }; - init_data.reserve_vertices(4); - init_data.reserve_indices(2); // 2 triangles - - // Quad corners in local frame (XY plane, will be rotated to match slicing plane) - Vec3f n = Vec3f::UnitZ(); - init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n); - init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n); - init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n); - init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n); - init_data.add_triangle(0, 1, 2); - init_data.add_triangle(0, 2, 3); - - m_slicing_plane.init_from(std::move(init_data)); - } - - GLShaderProgram* shader = wxGetApp().get_shader("flat"); - if (shader == nullptr) - return; - - glsafe(::glEnable(GL_DEPTH_TEST)); - glsafe(::glEnable(GL_BLEND)); - glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - - shader->start_using(); - - // Show a tilted plane representing the slicing direction. - // The slicing plane is rotated by belt_angle about the tilt axis from horizontal. - // Raise it slightly so it's visible above the bed surface. - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Transform3d model_matrix = Transform3d::Identity(); - model_matrix.translate(Vec3d(0., 0., 30.)); - model_matrix.rotate(Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis())); - - shader->set_uniform("view_model_matrix", view_matrix * model_matrix); - shader->set_uniform("projection_matrix", projection_matrix); - - m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue - m_slicing_plane.render(); - - glsafe(::glDisable(GL_BLEND)); - shader->stop_using(); -} - void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix) { // m_texture.reset(); @@ -915,15 +796,7 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr if (shader != nullptr) { shader->start_using(); - // Belt printer: rotate the default bed about X so the belt tilt is visible. - Transform3d view_model_matrix = view_matrix; - if (m_is_belt_printer && m_belt_angle > 0.f) { - double angle_rad = Geometry::deg2rad(static_cast(m_belt_angle)); - Transform3d belt_rotation = Transform3d::Identity(); - belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX())); - view_model_matrix = view_matrix * belt_rotation; - } - shader->set_uniform("view_model_matrix", view_model_matrix); + shader->set_uniform("view_model_matrix", view_matrix); shader->set_uniform("projection_matrix", projection_matrix); glsafe(::glEnable(GL_DEPTH_TEST)); diff --git a/src/slic3r/GUI/3DBed.hpp b/src/slic3r/GUI/3DBed.hpp index 788d8f52a3..4298ff8b1d 100644 --- a/src/slic3r/GUI/3DBed.hpp +++ b/src/slic3r/GUI/3DBed.hpp @@ -111,8 +111,6 @@ private: GLModel m_model; Vec3d m_model_offset{ Vec3d::Zero() }; GLModel m_gravity_arrow; - GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction - GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction Axes m_axes; float m_scale_factor{ 1.0f }; @@ -122,11 +120,6 @@ private: std::vector> m_extruder_shapes; std::vector m_extruder_heights; bool m_is_dark = false; - // Belt printer state for rendering. - bool m_is_belt_printer = false; - float m_belt_angle = 0.f; - // Tilt axis: 0 = X (belt travels along Y, the common case), 1 = Y. - int m_belt_tilt_axis = 0; public: Bed3D() = default; @@ -148,15 +141,6 @@ public: const BuildVolume& build_volume() const { return m_build_volume; } BuildVolume& build_volume() { return m_build_volume; } - // Belt printer bed settings. tilt_axis: 0 = X (belt along Y), 1 = Y. - void set_belt_printer(bool enabled, float angle_deg, int tilt_axis = 0) { - m_is_belt_printer = enabled; m_belt_angle = angle_deg; m_belt_tilt_axis = tilt_axis; - } - bool is_belt_printer() const { return m_is_belt_printer; } - float belt_angle() const { return m_belt_angle; } - // Unit vector of the tilt axis in bed space. - Vec3d belt_tilt_unit_axis() const { return m_belt_tilt_axis == 1 ? Vec3d::UnitY() : Vec3d::UnitX(); } - // Was the model provided, or was it generated procedurally? Type get_type() const { return m_type; } // Was the model generated procedurally? @@ -196,8 +180,6 @@ private: void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom); void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix); void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); - void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix); - void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix); // BBS: remove the bed picking logic // void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo); diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9193cc5ef8..d34b8c2c63 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1223,10 +1223,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const m_loaded_as_preview = false; // Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar - // menu item) from the loaded print here. Plater::set_bed_shape also calls set_belt_printer(), - // but only on bed-shape changes — not reliably on every slice/preview load — so the UI was - // staying hidden even though the (config-driven) designed view rendered. The tilt magnitude - // comes from the G-code header (gcode_result.belt_tilt_angle, abs of the slicing rotation). + // menu item) from the loaded print. The tilt magnitude comes from the G-code header + // (gcode_result.belt_tilt_angle, abs of the slicing rotation). m_belt_view_enabled = print.config().belt_printer.value; m_belt_angle_deg = gcode_result.belt_tilt_angle; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5b6e2c6300..cb5d2f7ace 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -14145,37 +14145,6 @@ void Plater::priv::set_bed_shape(const Pointfs &shape, Vec2d shape_position = partplate_list.get_current_shape_position(); bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position); - // Belt printer: configure build volume and bed rendering for belt mode. - { - const auto *belt_opt = config->option("belt_printer"); - bool is_belt = belt_opt && belt_opt->value; - if (is_belt) { - // The slicing rotation is the single source of truth for the belt tilt: - // its magnitude is the physical tilt angle and its axis is the tilt axis. - auto rot_axis = config->option>("belt_slice_rotation")->value; - double rot_angle = config->opt_float("belt_slice_rotation_angle"); - double belt_angle = std::abs(rot_angle); // physical tilt magnitude - int tilt_axis = (rot_axis == BeltRotationAxis::Y) ? 1 : 0; - bool infinite_y = config->opt_bool("belt_printer_infinite_y"); - bed.build_volume().set_belt_printer(true, belt_angle, infinite_y); - bed.set_belt_printer(true, static_cast(belt_angle), tilt_axis); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast(belt_angle)); - // The belt "designed view" back-transform is rebuilt from the print config at - // G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side - // inverse needs to be pushed to the viewer here. - } else { - // Reset the BuildVolume belt state too: Bed3D::set_shape early-returns when - // the bed params are unchanged, so a belt->normal switch (or toggling belt off - // on the same printer) would otherwise leave the BuildVolume with - // m_is_belt_printer=true and an inflated Y bbox, wrongly treating out-of-bounds - // objects as printable. Idempotent for a printer that was never belt. - bed.build_volume().set_belt_printer(false, 0., false); - bed.set_belt_printer(false, 0.f); - if (preview) - preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f); - } - } float prev_height_lid, prev_height_rod; partplate_list.get_height_limits(prev_height_lid, prev_height_rod); From ca934716f5db9a08e467a97cb2900f4ad1acbf7f Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 15:39:49 -0500 Subject: [PATCH 73/75] Plate icons: keep them inside the gap to the next plate The plate's icons, number and name scale with the plate's depth, but they sit in the gap to the next plate, which scales with its width. On a long, narrow bed (a 95 x 500 mm belt) they came out 40 mm wide and ran across the neighbouring plate. The scale is now also bounded by the gap, which leaves ordinary beds unchanged. --- src/slic3r/GUI/PartPlate.cpp | 19 +++++++++++++++---- src/slic3r/GUI/PartPlate.hpp | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 12f4e423e5..2b9bbbdf02 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -612,6 +612,17 @@ void PartPlate::calc_height_limit() { BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to create height limit top lines\n"; } +// The plate's icons and labels grow with its depth, but they sit in the gap to the +// next plate, which grows with its width: on a long, narrow bed (a belt) they would +// otherwise run across the neighbouring plate. +float PartPlate::icon_scale_factor() const +{ + const BoundingBoxf bed_ext = get_extents(m_shape); + const double by_depth = bed_ext.size().y() / 200.; + const double by_gap = bed_ext.size().x() * LOGICAL_PART_PLATE_GAP / (PARTPLATE_ICON_SIZE + 2 * PARTPLATE_ICON_GAP_LEFT); + return float(std::min(by_depth, by_gap)); +} + void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buffer) { buffer.reset(); @@ -628,7 +639,7 @@ void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buff #else //in the bottom auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[1]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float offset_y = PARTPLATE_TEXT_OFFSET_Y * factor; float offset_x = (one_number?PARTPLATE_TEXT_OFFSET_X1: PARTPLATE_TEXT_OFFSET_X2) * factor; @@ -650,7 +661,7 @@ void PartPlate::calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int ind ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz; float height = icon_sz; @@ -683,7 +694,7 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model) ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[2]; - auto factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float size = PARTPLATE_ICON_SIZE * factor; float gap_left = PARTPLATE_ICON_GAP_LEFT * factor; float gap_y = PARTPLATE_ICON_GAP_Y * factor; @@ -2589,7 +2600,7 @@ void PartPlate::generate_plate_name_texture() ExPolygon poly; auto bed_ext = get_extents(m_shape); Vec2d p = bed_ext[3]; - float factor = bed_ext.size()(1) / 200.0; + float factor = icon_scale_factor(); float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE; float width = icon_sz * m_name_texture.get_width() / m_name_texture.get_height(); // icon size * text_bb_ratio float height = icon_sz; // scale with icon size to preserve ratio while system scaling diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index babc88f364..67e8e300f5 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -178,6 +178,7 @@ private: void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model); void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox); void calc_height_limit(); + float icon_scale_factor() const; void calc_vertex_for_number(int index, bool one_number, GLModel &buffer); void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model); void calc_vertex_for_icons(int index, PickingModel &model); From 6a0d07664f60cb09a5ee6418584368b672a180d2 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 18:55:50 -0500 Subject: [PATCH 74/75] Belt purge tower: one prism per plate ensure_belt_purge_tower only ever looked at the current plate and kept a single prism, so the other plates had no tower and switching plates moved the one prism around. Every plate is now planned on its own: a prism that lies on no plate is stale, a plate whose prism matches its recorded inputs is left alone, the rest are deleted and recreated, highest index first. --- src/slic3r/GUI/BeltPurgeTower.cpp | 558 ++++++++++++++++-------------- src/slic3r/GUI/BeltPurgeTower.hpp | 9 +- src/slic3r/GUI/Plater.cpp | 4 +- 3 files changed, 313 insertions(+), 258 deletions(-) diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 3dc50d4cb2..e90491a972 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -33,28 +33,45 @@ namespace GUI { // is sized so each tilted slicing plane's cross-section through the prism can // absorb the worst-case purge volume of one layer; length follows the printed // objects along the belt (plus ramp/height compensation at both tilted ends). -bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig) +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs) { auto is_prism = [](const ModelObject *mo) { const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); return opt != nullptr && opt->getBool(); }; - std::vector prism_idxs; + // Every plate gets its own prism. A prism belongs to the plate it lies on; one + // that lies on no plate is stale. + const int plate_count = partplate_list.get_plate_count(); + sigs.resize(size_t(plate_count)); + std::vector> prisms_by_plate(static_cast(plate_count)); + std::vector stale_prisms; for (int i = 0; i < (int) model.objects.size(); ++i) - if (is_prism(model.objects[i])) - prism_idxs.push_back(i); + if (is_prism(model.objects[i])) { + const int plate_idx = model.objects[i]->instances.empty() ? -1 : partplate_list.find_instance(i, 0); + if (plate_idx >= 0 && plate_idx < plate_count) + prisms_by_plate[size_t(plate_idx)].push_back(i); + else + stale_prisms.push_back(i); + } // Deletes prism objects, keeping the sidebar and part plates in sync // (same primitives as Plater::priv::remove(), minus scene update — the - // caller refreshes the scene). - auto remove_prisms = [&](const std::vector &idxs) { + // caller refreshes the scene). Highest index first, so the others stay valid. + auto remove_prisms = [&](std::vector idxs) { + std::sort(idxs.begin(), idxs.end()); for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { model.delete_object(size_t(*it)); partplate_list.notify_instance_removed(*it, -1); obj_list->delete_object_from_list(size_t(*it)); } }; + auto all_prisms = [&]() { + std::vector all = stale_prisms; + for (const auto &v : prisms_by_plate) + all.insert(all.end(), v.begin(), v.end()); + return all; + }; // Cheap early-out for non-belt printers: only belt printers ever get a belt // purge tower, and this runs on every background-process tick — so avoid the @@ -63,10 +80,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object { const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); if (belt_pre == nullptr || !belt_pre->value) { - sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) + std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{}); + const std::vector all = all_prisms(); + if (all.empty()) return false; - remove_prisms(prism_idxs); + remove_prisms(all); return true; } } @@ -93,97 +111,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const auto *seq_opt = print_config.option>("print_sequence"); const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; - // Filaments used and bounding extent of the non-prism objects on the - // current plate (1-based filament ids; volume extruder 0 = object default). - PartPlate *plate = partplate_list.get_curr_plate(); - std::set filaments; - double x_min = std::numeric_limits::max(); - double x_max = -std::numeric_limits::max(); - double y_min = std::numeric_limits::max(); - double y_max = -std::numeric_limits::max(); - double z_max = 0.; - bool have_objects = false; - if (belt && plate != nullptr) { - for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { - const ModelObject *mo = model.objects[obj_idx]; - if (is_prism(mo)) - continue; - int obj_extruder = 1; - if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) - obj_extruder = opt->getInt(); - bool any_instance_on_plate = false; - for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { - if (!plate->contain_instance_totally(obj_idx, inst_idx)) - continue; - any_instance_on_plate = true; - const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); - x_min = std::min(x_min, bb.min.x()); - x_max = std::max(x_max, bb.max.x()); - y_min = std::min(y_min, bb.min.y()); - y_max = std::max(y_max, bb.max.y()); - z_max = std::max(z_max, bb.max.z()); - } - if (!any_instance_on_plate) - continue; - have_objects = true; - for (const ModelVolume *mv : mo->volumes) - for (int e : mv->get_extruders()) - filaments.insert(e > 0 ? e : obj_extruder); - } - } + // Mixed filament slots are expanded to their physical components below. + std::vector is_mixed; + std::vector comp_strs; + if (const auto *o = full_cfg.option("filament_is_mixed")) + is_mixed = o->values; + if (const auto *o = full_cfg.option("filament_mixed_components")) + comp_strs = o->values; - // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time - // ToolOrdering::resolve_mixed_filaments() replaces it with its physical - // components, so the toolchanges the prism has to absorb are between those - // components, not to the mixed slot itself. Counting the slot as a filament - // of its own therefore over-provisions the prism by one island per mixed slot - // -- the "extra purge tower" -- and, when every component is already used by - // another object, by an island that can never be reached at all. - // - // Expand here with the same helper the backend uses (Print.cpp's sequential - // path), so the GUI sizes the prism against the same filament set the slicer - // will actually produce. No-op when no filament is mixed. - { - // Copies, not references: a ternary with an empty-vector fallback would bind - // a reference to a temporary. - std::vector is_mixed; - std::vector comp_strs; - if (const auto *o = full_cfg.option("filament_is_mixed")) - is_mixed = o->values; - if (const auto *o = full_cfg.option("filament_mixed_components")) - comp_strs = o->values; - if (has_any_mixed_filament(is_mixed)) { - std::vector zero_based; - zero_based.reserve(filaments.size()); - for (int f : filaments) - if (f > 0) - zero_based.push_back((unsigned int) (f - 1)); - zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); - filaments.clear(); - for (unsigned int f : zero_based) - filaments.insert((int) f + 1); - } - } - - const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; - if (!wanted) { - sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) - return false; - remove_prisms(prism_idxs); - return true; - } - - // --- Sizing ----------------------------------------------------------- - const int n_islands = std::max(1, (int) filaments.size() - 1); - const double gap = 1.0; - // Every disconnected island needs at least 1 mm of printable width. Honor - // the configured total width whenever possible, but never let the island - // layout silently grow past the footprint used for placement. - const double min_width = n_islands + (n_islands - 1) * gap; - const double width = std::max(min_width, - print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); - const double printable_width = width - (n_islands - 1) * gap; + const double gap = 1.0; const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -201,15 +137,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object const double sin_t = std::sin(theta); const double cot_t = std::cos(theta) / sin_t; - // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. - const double belt_min = belt_is_y ? y_min : x_min; - const double belt_max = belt_is_y ? y_max : x_max; - const double lat_min = belt_is_y ? x_min : y_min; - const double lat_max = belt_is_y ? x_max : y_max; - - // Worst-case purge volume of one layer: up to (filament count - 1) - // toolchanges, each needing the worst flush matrix entry (mirrors the - // volume selection in Print::_plan_belt_purge()). // NOTE: both purge_in_prime_tower and single_extruder_multi_material are // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the // printer preset. Reading purge_in_prime_tower from the print preset returns @@ -218,71 +145,9 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // the backend Print::_plan_belt_purge() which reads both from the merged config. const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); - double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; - if (use_matrix) { - const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - const std::vector matrix = get_flush_volumes_matrix( - project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); - const auto * multi_opt = project_config.option("flush_multiplier"); - const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; - const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); - double m = 0.; - for (int i : filaments) - for (int j : filaments) - if (i != j && i <= n_total && j <= n_total) - m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); - if (m > 0.) - max_flush = m * multiplier; - } - const double v_layer = double(filaments.size() - 1) * max_flush; - - // Height from the per-layer purge demand. A tilted slicing plane cuts a - // printable_width x (height/sin) rectangle out of the bars, so one layer - // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that - // holds the worst-case per-layer purge, with eta (infill/perimeter packing) - // and a safety margin for the tilt ramps / grid-alignment slop, plus a - // minimum so the tower is a real printable body rather than a sliver. - const double eta = 0.85; - const double safety = 1.6; + const double prime_volume = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); - height = std::clamp(height, 8.0, std::max(8.0, printable_height)); - // --- Idempotence (input-keyed) ---------------------------------------- - auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization - BeltPurgeSignature new_sig; - new_sig.valid = true; - new_sig.filament_count = (int) filaments.size(); - new_sig.key[0] = q(width); - new_sig.key[1] = q(layer_h); - new_sig.key[2] = q(height); - new_sig.key[3] = q(belt_min); - new_sig.key[4] = q(belt_max); - new_sig.key[5] = q(lat_min); - new_sig.key[6] = q(z_max); - new_sig.key[7] = static_cast(rot); - new_sig.key[8] = std::lround(theta * 10000.0); - new_sig.key[9] = q(lat_max); - const Vec3d plate_origin = plate->get_origin(); - new_sig.key[10] = q(plate_origin.x()); - new_sig.key[11] = q(plate_origin.y()); - if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed = get_extents(bed_opt->values); - new_sig.key[12] = q(bed.max.x()); - new_sig.key[13] = q(bed.max.y()); - } - if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig) - return false; // already up to date — do not touch the model - - // --- Position ---------------------------------------------------------- - // Belt-travel axis. With the mesh rotated by theta before slicing, a machine - // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts - // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's - // FULL-cross-section region (the part not in a triangular end ramp) spans - // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' - // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and - // belt_end >= y_max + z_max*cot (parts' top features print further up the - // belt). The trailing z_max*cot term dominates the bar's own ramp. // The bed (printable_area) is plate-local but model instances live in the // plate's world frame, so the plate origin is added to every bed coordinate. const double inset = 1.; @@ -291,34 +156,225 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object bed_opt != nullptr && !bed_opt->values.empty()) bed_ext = get_extents(bed_opt->values); - // Along the belt the bar stops at the end of the plate: a longer bar cannot be - // printed, and the cross-sections it loses there are reported by the purge - // planner when the parts' last layers then purge more than the bar holds. - const double margin = 5.; - const double ramp_compensation = height / sin_t; - const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; - double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach - if (bed_ext.defined) - belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); - const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin - const double length = std::max(belt_end - belt_start, 10.); - belt_end = belt_start + length; - const double belt_center = 0.5 * (belt_start + belt_end); + // What each plate needs, decided before anything is deleted or created. + struct Plan + { + bool wanted = false; + BeltPurgeSignature sig; + int n_islands = 1; + double w_sub = 0., length = 0., height = 0.; + Vec3d center = Vec3d::Zero(); + }; + std::vector plans(static_cast(plate_count)); - // Across-belt: flush against the bed's maximum edge, inset by half the bar - // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). - const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - if (bed_ext.defined) { - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); - lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + Plan &plan = plans[size_t(plate_idx)]; + PartPlate *plate = partplate_list.get_plate(plate_idx); + + // Filaments used and bounding extent of the non-prism objects on this + // plate (1-based filament ids; volume extruder 0 = object default). + std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + // A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time + // ToolOrdering::resolve_mixed_filaments() replaces it with its physical + // components, so the toolchanges the prism has to absorb are between those + // components, not to the mixed slot itself. Counting the slot as a filament + // of its own therefore over-provisions the prism by one island per mixed slot + // -- the "extra purge tower" -- and, when every component is already used by + // another object, by an island that can never be reached at all. + // + // Expand here with the same helper the backend uses (Print.cpp's sequential + // path), so the GUI sizes the prism against the same filament set the slicer + // will actually produce. No-op when no filament is mixed. + if (has_any_mixed_filament(is_mixed)) { + std::vector zero_based; + zero_based.reserve(filaments.size()); + for (int f : filaments) + if (f > 0) + zero_based.push_back((unsigned int) (f - 1)); + zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs); + filaments.clear(); + for (unsigned int f : zero_based) + filaments.insert((int) f + 1); + } + + plan.wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!plan.wanted) + continue; + + // --- Sizing ----------------------------------------------------------- + const int n_islands = std::max(1, (int) filaments.size() - 1); + // Every disconnected island needs at least 1 mm of printable width. Honor + // the configured total width whenever possible, but never let the island + // layout silently grow past the footprint used for placement. + const double min_width = n_islands + (n_islands - 1) * gap; + const double width = std::max(min_width, + print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); + const double printable_width = width - (n_islands - 1) * gap; + + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + double max_flush = prime_volume; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // printable_width x (height/sin) rectangle out of the bars, so one layer + // slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with eta (infill/perimeter packing) + // and a safety margin for the tilt ramps / grid-alignment slop, plus a + // minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; + double height = safety * v_layer * sin_t / (printable_width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); + + // --- Idempotence (input-keyed) ---------------------------------------- + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature &new_sig = plan.sig; + new_sig.valid = true; + new_sig.filament_count = (int) filaments.size(); + new_sig.key[0] = q(width); + new_sig.key[1] = q(layer_h); + new_sig.key[2] = q(height); + new_sig.key[3] = q(belt_min); + new_sig.key[4] = q(belt_max); + new_sig.key[5] = q(lat_min); + new_sig.key[6] = q(z_max); + new_sig.key[7] = static_cast(rot); + new_sig.key[8] = std::lround(theta * 10000.0); + new_sig.key[9] = q(lat_max); + const Vec3d plate_origin = plate->get_origin(); + new_sig.key[10] = q(plate_origin.x()); + new_sig.key[11] = q(plate_origin.y()); + if (bed_ext.defined) { + new_sig.key[12] = q(bed_ext.max.x()); + new_sig.key[13] = q(bed_ext.max.y()); + } + + // --- Position ---------------------------------------------------------- + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and + // belt_end >= y_max + z_max*cot (parts' top features print further up the + // belt). The trailing z_max*cot term dominates the bar's own ramp. + // + // Along the belt the bar stops at the end of the plate: a longer bar cannot be + // printed, and the cross-sections it loses there are reported by the purge + // planner when the parts' last layers then purge more than the bar holds. + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_origin = plate_origin[belt_is_y ? 1 : 0]; + double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + if (bed_ext.defined) + belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset); + const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin + const double length = std::max(belt_end - belt_start, 10.); + belt_end = belt_start + length; + const double belt_center = 0.5 * (belt_start + belt_end); + + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed. lat_min/lat_max come from instance_bounding_box (world frame). + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + if (bed_ext.defined) { + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + } + + plan.n_islands = n_islands; + plan.w_sub = (width - (n_islands - 1) * gap) / n_islands; + plan.length = length; + plan.height = height; + plan.center = Vec3d(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); } - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, - belt_is_y ? belt_center : lat_center, - 0.5 * height); + // --- Decide --------------------------------------------------------------- + // A plate whose prism exists and matches its recorded inputs is left alone, so + // subsequent ticks are no-ops until the parts/config actually change. + std::vector to_delete = stale_prisms; + std::vector to_create; + for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) { + const Plan &plan = plans[size_t(plate_idx)]; + const std::vector &existing = prisms_by_plate[size_t(plate_idx)]; + BeltPurgeSignature &sig = sigs[size_t(plate_idx)]; + if (!plan.wanted) { + sig = BeltPurgeSignature{}; + to_delete.insert(to_delete.end(), existing.begin(), existing.end()); + } else if (existing.size() == 1 && model.objects[size_t(existing.front())]->instances.size() == 1 && plan.sig == sig) { + continue; + } else { + to_delete.insert(to_delete.end(), existing.begin(), existing.end()); + to_create.push_back(plate_idx); + } + } + if (to_delete.empty() && to_create.empty()) + return false; + remove_prisms(to_delete); + + // --- (Re)create ----------------------------------------------------------- // Build the prism as N DISCONNECTED sub-bars side by side across the belt, // N = (filaments - 1) = the worst-case number of toolchanges on one layer. // Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS, @@ -337,68 +393,64 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line // widths also keeps gap-fill from bridging them. 1 mm is about as close as // they can butt up while staying individually purgeable. - const double w_sub = (width - (n_islands - 1) * gap) / n_islands; + for (int plate_idx : to_create) { + const Plan &plan = plans[size_t(plate_idx)]; - // --- (Re)create --------------------------------------------------------- - if (!prism_idxs.empty()) - remove_prisms(prism_idxs); + TriangleMesh prism_mesh; + for (int i = 0; i < plan.n_islands; ++i) { + const double lat_off = i * (plan.w_sub + gap); + // Box dims: lateral = w_sub, along-belt = length, vertical = height. + TriangleMesh box = belt_is_y ? make_cube(plan.w_sub, plan.length, plan.height) // X = lateral, Y = belt + : make_cube(plan.length, plan.w_sub, plan.height); // X = belt, Y = lateral + box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); + prism_mesh.merge(box); + } - TriangleMesh prism_mesh; - for (int i = 0; i < n_islands; ++i) { - const double lat_off = i * (w_sub + gap); - // Box dims: lateral = w_sub, along-belt = length, vertical = height. - TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt - : make_cube(length, w_sub, height); // X = belt, Y = lateral - box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); - prism_mesh.merge(box); + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. + new_object->invalidate_bounding_box(); + new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(plan.center.x() - cur.center().x(), + plan.center.y() - cur.center().y(), + 0.0)); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + sigs[size_t(plate_idx)] = plan.sig; } - ModelObject *new_object = model.add_object(); - new_object->name = _u8L("Belt Purge Tower"); - new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); - new_volume->name = new_object->name; - - auto &cfg = new_object->config; - cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); - cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); - cfg.set_key_value("extruder", new ConfigOptionInt(1)); - // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — - // every extrusion is overriddable, so the absorbed volume matches the - // cross-section x layer-height estimate used for the height above. - cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); - cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); - cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); - cfg.set_key_value("enable_support", new ConfigOptionBool(false)); - cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); - cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); - cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); - - // Position by the belt-calibration pattern: drop to the bed, then translate - // the instance by the delta between the object's ACTUAL bbox center and the - // target. Setting the instance offset directly is unreliable here — the - // freshly added cube's local frame is not centered, so set_offset() lands - // the min corner (not the center) on the target, leaving the bar centered - // on the bed edge with half of it hanging off. - new_object->invalidate_bounding_box(); - new_object->ensure_on_bed(); - const BoundingBoxf3 cur = new_object->bounding_box_exact(); - new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), - desired_center.y() - cur.center().y(), - 0.0)); - new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); - - const size_t obj_idx = model.objects.size() - 1; - // Registers the object in the sidebar and notifies the part plates; - // selection is left untouched (auto-managed object). - obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); - - // Record the inputs that produced this prism so subsequent ticks are no-ops - // until the parts/config actually change. - sig = new_sig; - return true; } diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index 27d5b47c94..a009eb4d4b 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -1,5 +1,7 @@ #pragma once +#include + // ORCA-Belt: auto-managed purge prism for belt printers. // // Kept in its own translation unit (not buried in Plater.cpp) so it stays out @@ -34,12 +36,13 @@ struct BeltPurgeSignature } }; -// Keep the auto-generated belt purge prism in sync with the current config and -// plate contents. Creates / updates / removes the marked prism ModelObject. +// Keep the auto-generated belt purge prisms, one per plate, in sync with the +// current config and plate contents. Creates / updates / removes the marked prism +// ModelObjects; `sigs` holds the last inputs per plate index. // Returns true when the model was mutated (caller should refresh the scene). // Runs on every background-process update, so it is idempotent: it only mutates // the model when the desired prism differs from the cached signature in `sig`. -bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig); +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector &sigs); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cb5d2f7ace..b011d55865 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6993,7 +6993,7 @@ struct Plater::priv // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower // in BeltPurgeTower.cpp). Returns true when the model was mutated. bool ensure_belt_purge_tower(); - BeltPurgeSignature m_belt_purge_sig; + std::vector m_belt_purge_sigs; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -10633,7 +10633,7 @@ void Plater::priv::process_validation_warnings(const std::vectorobj_list(), m_belt_purge_sig); + return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sigs); } From 69e7cc1544c2ae7be32fdf39e2aee6667ee9fc7b Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 3 Oct 2026 19:19:02 -0500 Subject: [PATCH 75/75] Preview: judge a belt print's toolpaths by their back-transformed box all_paths_inside() accepts the path bounding box only within 3*EPSILON of the bed and otherwise tests every move; a belt print's moves are machine-frame coordinates whose Z is belt travel, so that test can never pass, and the designed view's min-corner anchor leaves the box a fraction of a millimetre below zero. Every multi-object belt plate therefore reported a path beyond the plate. The belt preview now judges the back-transformed box with a millimetre of room. --- src/slic3r/GUI/GCodeViewer.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index d34b8c2c63..89bc7e3fab 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1518,8 +1518,18 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const }); m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast(), libvgcode::convert(bbox[1]).cast()); - if (wxGetApp().is_editor()) - m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + if (wxGetApp().is_editor()) { + if (is_belt) { + // The moves are machine-frame coordinates (Z is belt travel), so the per-move + // test inside all_paths_inside() can never pass on a belt. Judge the + // back-transformed box instead, with room for the designed view's min-corner + // anchor, which is only accurate to a fraction of a millimetre. + BoundingBoxf3 bed = wxGetApp().plater()->build_volume().bounding_volume(); + bed.offset(1.); + m_contained_in_bed = !m_paths_bounding_box.defined || (bed.contains(m_paths_bounding_box.min) && bed.contains(m_paths_bounding_box.max)); + } else + m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box); + } m_extruders_count = gcode_result.filaments_count;