diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index 3f2507f24e..920790891c 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -376,15 +376,14 @@ static void belt_brim_band_paths(const BeltBrimContext &bc, // overhang outside the belt footprint and land in the brim ring, which the flattened // brim_object_gap - a belt-plane separation - does not cover. // -// THREADING: this runs inside posSupportMaterial, which Print::process() executes for all -// objects in a tbb::parallel_for (Print.cpp). Object slices are finished by then and safe -// to read across objects, but SUPPORT layers are not: another object's thread may be -// inside clear_support_layers() - which deletes the SupportLayer pointers - right now, so -// touching a foreign object's support_layers() here is a use-after-free. Only this -// object's own supports are consulted; they are complete, because make_belt_brim() runs at -// the tail of this object's own generate_support_material(). The cost is that the brim -// does not dodge a *different* object's support at the same Z, which needs the objects to -// overlap in the belt direction in the first place. +// SEQUENCING: this reads every object's layers and this object's own support layers. +// Another object's support step shifts that object's layer Z into the object frame for +// the duration of the run (PrintObject::_generate_support_material()), so the brims must +// not overlap with the parallel support step: Print::process() generates them one object +// after the other once that step is over (PrintObject::generate_belt_brim()), and an +// object that arrives on or leaves the plate invalidates the other brim owners' support +// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this +// object's supports are dodged; another object's support at the same Z is not. // `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole // objects are skipped without materialising their polygons. On a typical plate the // objects do not overlap and every foreign object drops out here, which matters because @@ -489,11 +488,19 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) - // The cut is the uphill edge of the first layer's contact band: everything - // past it belongs to later contacts. - bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, - bc.ctx.cutoff_u(object.layers().front()->print_z)); + if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { + // The cut is the uphill edge of the first contact's band: everything past it + // belongs to later contacts. The first contact is the first layer with + // geometry, not layers().front(): the slicing frame starts at the belt below + // the footprint, so the leading layers are empty and their contact lies ahead + // of the part. + const Layer *first_contact = nullptr; + for (const Layer *layer : object.layers()) + if (! layer->lslices.empty()) { first_contact = layer; break; } + if (first_contact == nullptr) + return; + bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z)); + } if (bc.region.empty()) return; diff --git a/src/libslic3r/BeltGCode.cpp b/src/libslic3r/BeltGCode.cpp index 1726fc6eac..e76e76b84f 100644 --- a/src/libslic3r/BeltGCode.cpp +++ b/src/libslic3r/BeltGCode.cpp @@ -25,7 +25,7 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print) // for the physical tilt the G-code viewer uses to enable belt view. file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str()); file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value); - // Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt + // Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt // tilt angle (or belt_frame_tilt_angle when decoupled). file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0); file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value); diff --git a/src/libslic3r/BeltSliceStrategy.cpp b/src/libslic3r/BeltSliceStrategy.cpp index 8aac729f1b..7b25ac53ec 100644 --- a/src/libslic3r/BeltSliceStrategy.cpp +++ b/src/libslic3r/BeltSliceStrategy.cpp @@ -73,8 +73,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo, z_shift.matrix()(2, 3) = z_shift_val; trafo = z_shift * trafo; } - // out_belt_min_z is only meaningful in belt mode; the standalone-remap path - // never reported it. + // out_belt_min_z is only meaningful in belt mode. if (out_belt_min_z && config.belt_printer.value) { *out_belt_min_z = (min_z != std::numeric_limits::max()) ? min_z : 0.; } diff --git a/src/libslic3r/BeltSliceStrategy.hpp b/src/libslic3r/BeltSliceStrategy.hpp index 33a7ed4979..1385818b5c 100644 --- a/src/libslic3r/BeltSliceStrategy.hpp +++ b/src/libslic3r/BeltSliceStrategy.hpp @@ -8,25 +8,23 @@ namespace Slic3r { -// Belt printer / pre-slice transform strategy. +// Belt printer pre-slice transform strategy. // -// Composes, in order, the pre-slice mesh transforms applied before slicing: -// 1. Pre-slice axis remap (standalone — works without belt mode) -// 2. Belt rotation (the sole mesh-side belt transform; shear & scale are a +// Composes, in order, the mesh transforms applied before slicing on a belt printer: +// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a // g-code-side stage, see MachineFrameTransform) -// 3. Per-object Z-shift that lifts the mesh above the build plate +// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the +// belt below its footprint // -// Isolates this belt/remap-specific logic from the generic slicing pipeline in +// Isolates this belt-specific logic from the generic slicing pipeline in // PrintObjectSlice.cpp. class BeltSliceStrategy { public: - // Apply the pre-slice remap + belt rotation + Z-shift to `trafo` in place. - // No-op when neither a remap nor a belt rotation is configured. + // Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt + // rotation is configured. // - // out_belt_min_z (if non-null) receives the minimum mesh Z after the - // transforms, but only in belt-printer mode — the standalone-remap path - // never reported it. + // out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms. static void apply_preslice_transforms(Transform3d &trafo, const PrintConfig &config, const ModelVolumePtrs &model_volumes, diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index d11e2fccdc..9863e80831 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -46,8 +46,6 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co // ---- Belt floor parameters ------------------------------------------------ -// Shared implementation for both PrintConfig and DynamicPrintConfig. -// Template avoids duplicating the math for the two config types. namespace { // Belt floor in the rotated slicer frame: the image of z_machine = 0 under R. @@ -81,33 +79,15 @@ inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, con } -template BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl( - const Config &config, const BoundingBoxf3 &bb, double original_height) + const PrintConfig &config, const BoundingBoxf3 &bb, double original_height) { BeltTransformPipeline::BeltHeightResult result; result.object_height = original_height; - // Extract the mesh rotation from config (the sole mesh-side belt transform). - BeltRotationAxis rot_axis; - double rot_angle; - - if constexpr (std::is_same_v) { - rot_axis = config.belt_slice_rotation.value; - rot_angle = config.belt_slice_rotation_angle.value; - } else { - // DynamicPrintConfig path - auto get_float = [&](const char *key) { - auto *opt = config.template option(key); - return opt ? opt->value : 0.0; - }; - auto get_rot_axis = [&](const char *key) { - auto *opt = config.template option>(key); - return opt ? opt->value : BeltRotationAxis::None; - }; - rot_axis = get_rot_axis("belt_slice_rotation"); - rot_angle = get_float("belt_slice_rotation_angle"); - } + // The mesh rotation (the sole mesh-side belt transform). + const BeltRotationAxis rot_axis = config.belt_slice_rotation.value; + const double rot_angle = config.belt_slice_rotation_angle.value; bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON; if (!has_rotation) @@ -159,12 +139,6 @@ BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_heig return compute_belt_height_and_floor_impl(config, bbox, original_height); } -BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, double original_height) -{ - return compute_belt_height_and_floor_impl(config, bbox, original_height); -} - bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out) { out = BeltFloorParams{}; diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 7409fb2c7f..1aae8ba39b 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -82,12 +82,6 @@ public: return t; } - static PhysicalTilt physical_tilt(const PrintConfig &config) - { - return physical_tilt(config.belt_slice_rotation.value, - config.belt_slice_rotation_angle.value); - } - // ---- Matrix builders -------------------------------------------------- // Build the 3x3 rotation matrix from belt_slice_rotation* config. @@ -133,11 +127,6 @@ public: static BeltHeightResult compute_belt_height_and_floor( const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height); - - // Overload for DynamicPrintConfig (used by static slicing_parameters). - static BeltHeightResult compute_belt_height_and_floor( - const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, - double original_height); }; } // namespace Slic3r diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index c96d299211..c92ebc8e1c 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2222,6 +2222,22 @@ void GCode::PlaceholderParserIntegration::validate_output_vector_variables() // Collect pairs of object_layer + support_layer sorted by print_z. // object_layer & support_layer are considered to be on the same print_z, if they are not further than EPSILON. +// Belt printers: whether an object layer writes anything, its own extrusions or a belt +// brim band riding on it. Shared by collect_layers_to_print() (which drops the layers +// that do not) and the layer count. +static bool belt_object_layer_prints_something(const PrintObject &object, const Layer &layer) +{ + if (layer.has_extrusions()) + return true; + if (object.has_belt_brim()) { + const auto &by_layer = object.belt_brim_by_layer(); + const size_t id = layer.id(); + if (id < by_layer.size() && ! by_layer[id].empty()) + return true; + } + return false; +} + std::vector GCode::collect_layers_to_print(const PrintObject& object, bool skip_empty_first_layer) { std::vector layers_to_print; @@ -2378,6 +2394,17 @@ std::vector GCode::collect_layers_to_print(const PrintObjec PrintStateBase::WarningLevel::CRITICAL, warning, PrintStateBase::SlicingEmptyGcodeLayers); } + // Belt printers: drop the layers that print nothing (see the by-layer overload), so + // the by-object export writes the same layer changes as the by-layer one. + if (object.print()->config().belt_printer.value) + layers_to_print.erase( + std::remove_if(layers_to_print.begin(), layers_to_print.end(), [&object](const LayerToPrint <p) { + return ! ((ltp.object_layer != nullptr && belt_object_layer_prints_something(object, *ltp.object_layer)) || + (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) || + (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty())); + }), + layers_to_print.end()); + return layers_to_print; } @@ -2404,6 +2431,9 @@ std::vector>> GCode::collec errors.push_back(e); continue; } + // On a belt an object may be left without a layer to print at all. + if (per_object[i].empty()) + continue; OrderingItem ordering_item; ordering_item.object_idx = i; ordering.reserve(ordering.size() + per_object[i].size()); @@ -2448,18 +2478,13 @@ std::vector>> GCode::collec // moves it sees, so a gap folds every later layer into the one before it. if (print.config().belt_printer.value) { auto prints_something = [](const LayerToPrint <p) { - if (ltp.object_layer != nullptr && ltp.object_layer->has_extrusions()) + if (ltp.object_layer != nullptr && ltp.original_object != nullptr && + belt_object_layer_prints_something(*ltp.original_object, *ltp.object_layer)) return true; if (ltp.support_layer != nullptr && ltp.support_layer->has_extrusions()) return true; if (ltp.belt_brim_band != nullptr && ! ltp.belt_brim_band->fills.empty()) return true; - if (ltp.object_layer != nullptr && ltp.original_object != nullptr && ltp.original_object->has_belt_brim()) { - const auto &by_layer = ltp.original_object->belt_brim_by_layer(); - const size_t id = ltp.object_layer->id(); - if (id < by_layer.size() && ! by_layer[id].empty()) - return true; - } return false; }; layers_to_print.erase( @@ -3192,8 +3217,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato if (print.config().belt_printer.value) { m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) { const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero(); - return this->on_first_layer(Vec3d(point_logical.x() - m_origin.x() + extruder_offset.x(), - point_logical.y() - m_origin.y() + extruder_offset.y(), + // The writer hands over the point with the plate origin (its XY offset) already + // taken off, while m_origin still carries it: take off the instance part only. + const Vec2d plate_offset = m_writer.get_xy_offset().cast(); + return this->on_first_layer(Vec3d(point_logical.x() - (m_origin.x() - plate_offset.x()) + extruder_offset.x(), + point_logical.y() - (m_origin.y() - plate_offset.y()) + extruder_offset.y(), point_logical.z())); }); } @@ -3201,18 +3229,26 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // How many times will be change_layer() called? // change_layer() in turn increments the progress bar status. m_layer_count = 0; + // On a belt, collect_layers_to_print() drops the layers that print nothing (an + // object's empty lead-in), so they must not be counted here either or the layer + // count in the file disagrees with its layer changes. + const bool belt = print.config().belt_printer.value; if (print.config().print_sequence == PrintSequence::ByObject) { // Add each of the object's layers separately. for (auto object : print.objects()) { std::vector zs; zs.reserve(object->layers().size() + object->support_layers().size()); for (auto layer : object->layers()) - zs.push_back(layer->print_z); + if (! belt || belt_object_layer_prints_something(*object, *layer)) + zs.push_back(layer->print_z); for (auto layer : object->support_layers()) - zs.push_back(layer->print_z); + if (! belt || layer->has_extrusions()) + zs.push_back(layer->print_z); // Belt brim apron bands each get their own change_layer() call. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); + if (zs.empty()) + continue; std::sort(zs.begin(), zs.end()); //BBS: merge numerically very close Z values. auto end_it = std::unique(zs.begin(), zs.end()); @@ -3229,9 +3265,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato for (auto object : print.objects()) { zs.reserve(zs.size() + object->layers().size() + object->support_layers().size()); for (auto layer : object->layers()) - zs.push_back(layer->print_z); + if (! belt || belt_object_layer_prints_something(*object, *layer)) + zs.push_back(layer->print_z); for (auto layer : object->support_layers()) - zs.push_back(layer->print_z); + if (! belt || layer->has_extrusions()) + zs.push_back(layer->print_z); // See the ByObject branch: apron bands are real printed layers. for (const BeltBrimBand &band : object->belt_brim_prologue()) zs.push_back(band.print_z); @@ -4173,6 +4211,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato // Reset the cooling buffer internal state (the current position, feed rate, accelerations). m_cooling_buffer->set_current_extruder(initial_extruder_id, get_extruder_id(initial_extruder_id)); m_cooling_buffer->reset(this->writer().get_position()); + // The belt first-layer band is tracked per object as well: if the previous + // object ended inside the band, this one has to open its own. + m_belt_in_band = false; // Process all layers of a single object instance (sequential mode) with a parallel pipeline: // Generate G-code, run the filters (vase mode, cooling buffer), run the G-code analyser // and export G-code into file. @@ -7318,6 +7359,9 @@ LayerResult GCode::process_layer( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); + // Same as the main instance loop: a belt printer rotates the origin through + // the belt transform (BeltGCode::on_set_origin). + this->on_set_origin(&instance_to_print.print_object, offset); // --- Build emission plan --- // Each entry represents one travel_to_z + extrude pass. Per-object mode produces diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 4aa96f9a4c..da78aa0a35 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -393,13 +393,6 @@ public: } }; - // Public accessor for the first-layer plane evaluator. Used by - // CoolingBuffer (which is constructed with a GCode reference and needs - // to read the plane for per-segment fan re-evaluation). All other - // first-layer-plane access points (on_first_layer overload, effective - // index helper) are in the protected section since they're called from - // GCode internals only. - protected: class GCodeOutputStream { public: @@ -839,12 +832,6 @@ protected: std::unique_ptr m_cooling_buffer; std::unique_ptr m_spiral_vase; - // First-layer plane evaluator. Constructed once per print from the - // PrintConfig. is_active() == false on non-belt printers and on belt - // printers without a Z-axis shear; in that case all per-path plane - // checks short-circuit to the legacy Layer::id() == 0 path. - // Plate origin, kept so a writer replaced during export can be given it again. - std::unique_ptr m_pressure_equalizer; std::unique_ptr m_pa_processor; diff --git a/src/libslic3r/GCode/BeltBackTransform.hpp b/src/libslic3r/GCode/BeltBackTransform.hpp index 3956450a9f..236ad84f51 100644 --- a/src/libslic3r/GCode/BeltBackTransform.hpp +++ b/src/libslic3r/GCode/BeltBackTransform.hpp @@ -28,9 +28,6 @@ public: // no back-transform is active. Vec3d apply(const Vec3d &pos) const; - // True if a non-identity back-transform is active. - bool is_active() const { return m_active; } - private: bool m_active = false; Transform3d m_inverse = Transform3d::Identity(); diff --git a/src/libslic3r/GCode/BeltKinematics.cpp b/src/libslic3r/GCode/BeltKinematics.cpp index c36c088658..27d5dad105 100644 --- a/src/libslic3r/GCode/BeltKinematics.cpp +++ b/src/libslic3r/GCode/BeltKinematics.cpp @@ -10,12 +10,8 @@ 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_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 @@ -25,15 +21,6 @@ Vec3d BeltKinematics::to_machine(const Vec3d &p) const 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)); diff --git a/src/libslic3r/GCode/BeltKinematics.hpp b/src/libslic3r/GCode/BeltKinematics.hpp index 8f10bd1074..7dc08635ab 100644 --- a/src/libslic3r/GCode/BeltKinematics.hpp +++ b/src/libslic3r/GCode/BeltKinematics.hpp @@ -31,12 +31,6 @@ 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 @@ -47,14 +41,9 @@ public: // coordinates and G2/G3 cannot describe it. bool supports_arc_moves() const override { return false; } - 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 }; }; diff --git a/src/libslic3r/GCode/MachineKinematics.cpp b/src/libslic3r/GCode/MachineKinematics.cpp index 063020fe8a..d9b4b65513 100644 --- a/src/libslic3r/GCode/MachineKinematics.cpp +++ b/src/libslic3r/GCode/MachineKinematics.cpp @@ -17,32 +17,9 @@ Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const 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 index 5c53bd9f58..fc13282998 100644 --- a/src/libslic3r/GCode/MachineKinematics.hpp +++ b/src/libslic3r/GCode/MachineKinematics.hpp @@ -24,17 +24,6 @@ public: // 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. @@ -72,8 +61,6 @@ 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(); } bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); } @@ -93,7 +80,6 @@ public: 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 }; diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 979371e304..f61587d562 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1023,7 +1023,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto continue; LayerTools &layer_tools = this->tools_for_layer(band.print_z); layer_tools.extruders.push_back(brim_filament); - layer_tools.has_belt_brim = true; } } @@ -1042,7 +1041,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto continue; LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z); layer_tools.extruders.push_back(brim_filament); - layer_tools.has_belt_brim = true; } } diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 3d42457e1a..5c11f482cd 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -179,10 +179,6 @@ public: // Should a skirt be printed at this layer? // Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed. bool has_skirt = false; - // Belt printers: is this one of the brim-only apron layers below the object's - // first layer? Kept separate from has_object so skirt marking and wiping - // overrides are unaffected. - bool has_belt_brim = false; // Will there be anything extruded on this layer for the wipe tower? // Due to the support layers possibly interleaving the object layers, // wipe tower will be disabled for some support only layers. diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 88be72a6f1..12a56dacdc 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -236,8 +236,8 @@ protected: Vec3d apply_axis_remap(const Vec3d &pos) const; // Motion uses the global/base process variant until a filament becomes active. - // Protected so subclasses index the per-extruder speed options (travel_speed, - // travel_speed_z, initial_layer_travel_speed) exactly as the base writer does. + // Indexes the per-extruder speed options (travel_speed, travel_speed_z, + // initial_layer_travel_speed). size_t m_cached_extruder_idx; private: diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 41db0ed9db..b1c6ed21fa 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -1839,8 +1839,10 @@ void PerimeterGenerator::process_classic() bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; if (is_outer_wall_first || //BBS: always print outer wall first when there indeed has brim. + // btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact). (this->layer_id == 0 && - this->object_config->brim_type == BrimType::btOuterOnly && + (this->object_config->brim_type == BrimType::btOuterOnly || + this->object_config->brim_type == BrimType::btLeadingEdgeOnly) && this->object_config->brim_width.value > 0)) entities.reverse(); // Orca: sandwich mode. Apply after 1st layer. diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index f60f6b4e19..fae1fcdb0b 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -231,12 +231,6 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor(data) {} }; -// The model's raw bounding box, in the frame the belt floor parameters refer to. -inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig & /*config*/) -{ - return model_object.raw_bounding_box(); -} - // Single instance of a PrintObject. // As multiple PrintObjects may be generated for a single ModelObject (their instances differ in rotation around Z), // ModelObject's instancess will be distributed among these multiple PrintObjects. @@ -437,10 +431,7 @@ public: // the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id // over the printing regions, 1 if none is explicitly set. unsigned int belt_brim_filament() const; - // False when this object's instances sit at different points ALONG the belt, which - // would need a separate set of bands each. Public so validate() can explain it. const std::vector& belt_brim_by_layer() const { return m_belt_brim_by_layer; } - const std::vector& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; } const std::vector& belt_brim_prologue() const { return m_belt_brim_prologue; } void clear_belt_brim(); void set_belt_brim(std::vector &&by_layer, @@ -721,8 +712,6 @@ private: bool m_belt_brim_pending { false }; // Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc. double m_belt_min_z { 0.0 }; - // Belt printer: XY correction from global pre-slice mode, applied to G-code origin. - Vec2d m_belt_global_xy_correction { Vec2d::Zero() }; // Belt printer: exact belt_floor_z_shift computed during posSlice from a // vertex-level scan of the post-transform mesh. Cached separately from // m_slicing_params so that rebuilding m_slicing_params on a non-belt-affecting @@ -733,8 +722,6 @@ private: bool m_belt_floor_z_shift_cache_valid { false }; public: double belt_global_z_offset() const { return m_belt_global_z_offset; } - double belt_min_z() const { return m_belt_min_z; } - Vec2d belt_global_xy_correction() const { return m_belt_global_xy_correction; } private: diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index 39d5716b08..096f505542 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1916,6 +1916,13 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } if (new_objects || deleted_objects) update_apply_status(this->invalidate_steps({ psSkirtBrim, psWipeTower, psGCodeExport })); + // A belt brim is clipped against the other objects on the plate (BeltBrim.cpp, + // belt_brim_obstacles), and it is rebuilt with its object's support step: an + // object that arrived or left changes every other brim owner's brim. + if ((new_objects || deleted_objects) && m_config.belt_printer.value) + for (PrintObject *object : m_objects) + if (object->has_belt_brim()) + update_apply_status(object->invalidate_step(posSupportMaterial)); if (new_objects) update_apply_status(false); print_regions_reshuffled = true; @@ -1930,9 +1937,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ } } - // Belt printer global mode: when any object's instances shifted, - // recompute m_belt_global_z_offset for ALL objects (it depends on - // min_shift across all objects, so one move affects everyone). + // Belt printer: when any object's instances shifted, re-slice every object. + // The global Z offset follows each object's position along the belt, and the + // belt brims are clipped against the other objects. if (belt_instances_shifted && m_config.belt_printer.value) { for (PrintObject *object : m_objects) update_apply_status(object->invalidate_step(posSlice)); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 503e7c464b..c95ee6f5ce 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7414,8 +7414,8 @@ void PrintConfigDef::init_fff_params() def = this->add("belt_frame_tilt_angle", coFloat); def->label = L("Machine-frame tilt angle"); def->category = L("Printable space"); - def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (tan) and " - "scale (1/cos) applied to G-code. Only used when 'Decouple machine-frame " + def->tooltip = L("Tilt angle (degrees) used to derive the machine-frame shear (cot) and " + "scale (1/|sin|) applied to G-code. Only used when 'Decouple machine-frame " "tilt' is enabled; otherwise the belt tilt angle is used."); def->sidetext = L("°"); def->min = -89.9; @@ -7423,23 +7423,29 @@ void PrintConfigDef::init_fff_params() def->mode = comExpert; def->set_default_value(new ConfigOptionFloat(45.)); - // G-code axis remap with sign. Each field is its own row in the settings tab. - auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, - RemapAxis default_axis, ConfigOptionMode mode = comSimple) { + // G-code axis remap with sign. Each field is its own row in the settings tab. The + // labels and tooltips are literals in L() so they are extracted for translation. + auto add_belt_remap = [this](const char *key, const std::string &label, const std::string &tooltip, + RemapAxis default_axis, ConfigOptionMode mode) { auto def = this->add(key, coEnum); - def->label = L(label); + def->label = label; def->category = L("Printable space"); - def->tooltip = L(tooltip); + def->tooltip = tooltip; def->enum_keys_map = &ConfigOptionEnum::get_enum_values(); def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; def->mode = mode; // Visibility may also be gated by toggle_line in Tab.cpp def->set_default_value(new ConfigOptionEnum(default_axis)); }; - - add_belt_remap("gcode_remap_x", "G-code remap 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", "G-code remap 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", "G-code remap Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop); + add_belt_remap("gcode_remap_x", L("G-code remap X"), + L("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", L("G-code remap Y"), + L("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", L("G-code remap Z"), + L("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 @@ -7471,7 +7477,7 @@ void PrintConfigDef::init_fff_params() def->label = L("Belt purge tower width"); def->category = L("Printable space"); def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically " - "generated on belt printers when the prime tower is enabled and multiple " + "generated on belt printers when the belt purge tower is enabled and multiple " "filaments are used. Filament-change purging is routed into this prism's " "extrusions instead of a classic wipe tower. Its height is computed " "automatically from the worst-case purge volume per layer: a wider prism " diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 4f46921c3f..c2abb5466a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1373,7 +1373,10 @@ bool PrintObject::invalidate_state_by_config_options( const auto* new_brim_type = new_config.option>(opt_key); //BBS: When switch to manual brim, the object must have brim, then re-generate perimeter //to make the wall order of first layer to be outer-first - if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly) + // btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it + // takes part in the same first-layer wall order rule. + if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly || + old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly) steps.emplace_back(posPerimeters); } } else if ( @@ -1767,7 +1770,11 @@ bool PrintObject::invalidate_step(PrintObjectStep step) // posSimplifySupportPath is listed with posSupportMaterial: invalidate_steps() does not // propagate, so without it a re-slice regenerated the supports but kept the step done, // and the new support paths were exported unsimplified, unlike a fresh slice. - invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath }); + // posDetectOverhangsForLift reads the layers' overhang regions, which a re-slice + // starts over empty: without it here the step stayed done and the lift logic in + // GCode::needs_retraction() had no overhangs to test against until something else + // invalidated it. + invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill, posSimplifySupportPath, posDetectOverhangsForLift }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); m_slicing_params.valid = false; // The exact belt_floor_z_shift is recomputed when slice() runs again. diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 5f70887c9c..facce01787 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -902,7 +902,6 @@ void PrintObject::slice() // them. m_belt_min_z = 0.; m_belt_global_z_offset = 0.; - m_belt_global_xy_correction = Vec2d::Zero(); this->clear_layers(); m_layers = new_layers(this, generate_object_layers(m_slicing_params, layer_height_profile, m_config.precise_z_height.value)); this->slice_volumes(); @@ -910,19 +909,10 @@ void PrintObject::slice() // Belt floor Z-shift: where is the belt surface in final slicer space? // - // The belt surface is at model_Y=0 (XZ belt plane). After the full - // pipeline (trafo_centered → pre_remap → shear → z_shift), the belt - // surface equation in slicer space is: - // Z_belt = sf * from_axis + belt_surface_z_centered + z_shift_val - // - // belt_surface_z_centered = remapped_bbox.min.z() (the Z position of - // the belt surface in centered-pre-shear slicer space, which is 0 - // without pre-remap but nonzero when e.g. Y↔Z swap shifts the belt - // surface away from Z=0 by the centering offset). - // - // z_shift_val = max(0, -m_belt_min_z) (lifts mesh above Z=0). - // - // So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val + // The belt surface is the model's Z=0 plane. After the belt rotation and the + // Z-shift it is the plane Z_belt = shear_factor * from_axis + z_shift_val in + // slicer space, with z_shift_val = max(0, -m_belt_min_z), the lift that starts + // the slicing frame at the belt below the footprint. if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) { double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.; // The belt surface is at Z=0 in centered slicer space and bb.min.z() is @@ -968,10 +958,10 @@ void PrintObject::slice() if (m_layers.empty()) throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n")); - // Belt printer global mode: offset all layer Z values so objects at - // different bed positions print at different heights on the tilted belt. - // This is a post-slicing adjustment — the sliced geometry is identical - // regardless of global mode, only the output Z coordinates change. + // Belt printer: offset all layer Z values so objects at different positions + // along the belt print at different heights on the tilted belt. This is a + // post-slicing adjustment: the sliced geometry is the same, only the output Z + // coordinates change. { const auto &pcfg = this->print()->config(); BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value @@ -995,7 +985,6 @@ void PrintObject::slice() // couples slicer_z back into both machine_y and machine_z. Compensating // layer.print_z by belt_z_shift here makes the back-transform produce // correct machine-frame coordinates whether or not a global mode is active. - const double belt_surface_z = 0.; // the belt surface is Z=0 in centered slicer space // The compensation must mirror the Z-shift actually applied, which // is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE // slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or @@ -1003,7 +992,7 @@ void PrintObject::slice() // no lift was applied, and an unclamped m_belt_min_z here would // leak straight into the layer Z values, floating the whole object // off the belt by exactly that amount. - double belt_z_shift = std::min(m_belt_min_z, 0.) - belt_surface_z; + double belt_z_shift = std::min(m_belt_min_z, 0.); // the belt surface is Z=0 in centered slicer space double global_z_offset = belt_z_shift; // Centering correction: trafo_centered pretranslates by @@ -1034,7 +1023,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(); - m_belt_global_xy_correction = Vec2d(c.x(), c.y()); BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=(" << c.x() << ", " << c.y() << ", " << c.z() << ")" @@ -1042,7 +1030,7 @@ void PrintObject::slice() } BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset - << " (relative to min across " << this->print()->objects().size() << " objects)"; + << " (" << this->print()->objects().size() << " objects on the plate)"; m_belt_global_z_offset = global_z_offset; if (std::abs(global_z_offset) > EPSILON) { for (Layer *layer : m_layers) diff --git a/src/libslic3r/Support/TreeSupport3D.cpp b/src/libslic3r/Support/TreeSupport3D.cpp index dc284ba94f..ef267fba68 100644 --- a/src/libslic3r/Support/TreeSupport3D.cpp +++ b/src/libslic3r/Support/TreeSupport3D.cpp @@ -3952,6 +3952,13 @@ void organic_draw_branches( const double tiny_area = tiny_area_threshold(); //FIXME parallelize? for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) { + // ORCA: safety offset when trimming collision/bed to improve robustness. + slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); + slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes); + // Belt floor: clip branch slices against the belt surface plane. + LayerIndex belt_idx = layer_begin + i; + if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty()) + slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]); remove_small(slices[i], tiny_area); } diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index cdf105fa69..3217edf1da 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -896,8 +896,7 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod } m_writer = std::move(belt_writer); } 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. + // Previously configured for a belt printer; drop back to a plain writer. m_writer = std::make_shared(); } diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index 1f1c7844dc..4ef676cdb1 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -370,9 +370,9 @@ private: const Calib_Params &m_params; - // 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. + // Belt printers get belt kinematics installed on it (_refresh_writer); + // shared_ptr keeps the class copyable — the writer is rebuilt by + // refresh_setup() before every use anyway. std::shared_ptr m_writer{std::make_shared()}; Vec3d m_starting_point; bool m_is_start_point_fixed = false; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 86ed871ed6..fadcd11f56 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -17017,11 +17017,38 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) { } +// The belt provini tower (Calib_Params::test_model 1) is one embossed model per +// temperature range. +static std::string belt_temp_tower_asset(const Calib_Params ¶ms) +{ + const int t_start = (int) lround(params.start); + const int t_end = (int) lround(params.end); + return Slic3r::resources_dir() + "/calib/temperature_tower/belt_temp_tower_" + + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; +} + void Plater::calib_temp(const Calib_Params& params) { constexpr double base_temp_tower_nozzle_diameter = 0.4; constexpr double base_temp_tower_block_height = 10.0; constexpr int base_temp_tower_temp_step = 5; + // A belt provini tower exists only for the ranges it was embossed for, and another + // range's model would print numbers that do not match its temperatures. Refuse + // before the current project is replaced. + if (params.mode == CalibMode::Calib_Temp_Tower && params.test_model >= 1) { + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + if (printer_config.has("belt_printer") && printer_config.opt_bool("belt_printer") && + ! boost::filesystem::exists(belt_temp_tower_asset(params))) { + MessageDialog dlg(static_cast(wxGetApp().mainframe), + format_wxstr(_L("No belt temperature tower is available for the range %1% to %2% °C. " + "Use a range the tower models cover, for example 230 to 190."), + (int) lround(params.start), (int) lround(params.end)), + _L("Temperature tower"), wxICON_ERROR | wxOK); + dlg.ShowModal(); + return; + } + } + const auto calib_temp_name = _L("Nozzle temperature test"); new_project(false, false, calib_temp_name); wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); @@ -17077,13 +17104,9 @@ void Plater::calib_temp(const Calib_Params& params) { temps.push_back(t); if (temps.empty()) temps.push_back(t_start); - const std::string calib_dir = Slic3r::resources_dir() + "/calib/temperature_tower/"; - std::string asset = calib_dir + "belt_temp_tower_" + std::to_string(t_start) + "_" + std::to_string(t_end) + ".stl"; - if (!boost::filesystem::exists(asset)) { - BOOST_LOG_TRIVIAL(warning) << "[belt_temp] no embossed provini for " << t_start << "->" << t_end - << ", falling back to 230_190 (embossed numbers will not match)"; - asset = calib_dir + "belt_temp_tower_230_190.stl"; - } + const std::string asset = belt_temp_tower_asset(params); + if (!boost::filesystem::exists(asset)) // refused above, before new_project() + return; if (!add_model(false, asset) || model().objects.empty()) return; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index bd19a62024..f5a0e62458 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -6349,6 +6349,10 @@ void TabPrinter::toggle_options() // Belt printer: show belt-specific settings only when belt_printer is enabled. bool is_belt = m_config->opt_bool("belt_printer"); + // update_fff() derives build_plate_tilt_{x,y} from the belt tilt on a belt + // printer, so an edit here would be overwritten; keep them read-only there. + toggle_option("build_plate_tilt_x", !is_belt); + toggle_option("build_plate_tilt_y", !is_belt); bool expert_or_above = (m_mode >= comExpert); toggle_line("belt_printer_infinite_y", is_belt); // Belt tilt: the sole mesh-side belt transform (visible by default in belt mode). diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 32c37882f5..58f8d12e39 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -31,10 +31,12 @@ #include "libslic3r/Support/TreeModelVolumes.hpp" #include "libslic3r/Support/TreeSupportCommon.hpp" #include "libslic3r/Support/BeltFloorContext.hpp" +#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Polyline.hpp" #include #include +#include #include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" @@ -1244,6 +1246,98 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above CHECK(collides(last + num_raft)); } +// organic_draw_branches() trims every branch slice against the collision volume (the +// part grown by the support XY distance), the bed and, on a belt, the belt plane before +// it becomes support, so a branch never runs into the part it supports. Not a belt +// feature: this is the generator every printer uses. +TEST_CASE("Organic tree supports keep their distance from the part", "[Print][Support]") +{ + // A 20 mm cube carrying a 60 x 60 mm plate: a 20 mm wide ceiling all around the + // cube, 16 mm above the bed, with the cube's four corners in the way of the branches + // that drop from it. The plate reaches into the cube so the two shells overlap + // instead of sharing a face. + indexed_triangle_set its = its_make_cube(20., 20., 20.); + indexed_triangle_set plate = its_make_cube(60., 60., 4.); + its_translate(its, Vec3f(20.f, 20.f, 0.f)); + its_translate(plate, Vec3f(0.f, 0.f, 16.f)); + its_merge(its, plate); + TriangleMesh mesh(std::move(its)); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "skirt_loops", 0 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "support_threshold_angle", 30 }, + }); + Print print; + Model model; + init_print({ mesh }, print, model, config); + // On the bed, not at its corner (the fixture leaves the object at the origin). + model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &object = *print.objects().front(); + INFO("object layers " << object.layers().size() << ", support layers " << object.support_layers().size()); + REQUIRE(! object.support_layers().empty()); + // Support exists under the plate at all. + size_t support_layers_with_fills = 0; + for (const SupportLayer *layer : object.support_layers()) + if (! layer->support_fills.empty()) + ++ support_layers_with_fills; + INFO("support layers with extrusions " << support_layers_with_fills); + CHECK(support_layers_with_fills > 20); + + // Object layers by print_z, to look up the part's slice at a support layer's height. + std::map object_layers; + for (const Layer *layer : object.layers()) + object_layers[scaled(layer->print_z)] = layer; + auto contains = [](const ExPolygons &expolys, const Point &pt) { + for (const ExPolygon &ex : expolys) + if (ex.contains(pt)) + return true; + return false; + }; + // No support extrusion may run closer to the part's slice than half a line width: + // the generator keeps the support XY distance (0.35 mm by default) plus the line's + // own half width away from it. + const float min_gap = scaled(0.2); + size_t too_close = 0, points = 0, layers_checked = 0, layers_unmatched = 0; + for (const SupportLayer *layer : object.support_layers()) { + if (layer->support_fills.empty()) + continue; + // The object layer whose slab spans this support layer's height. + auto it = object_layers.lower_bound(scaled(layer->print_z - EPSILON)); + if (it == object_layers.end()) { + ++ layers_unmatched; + continue; + } + ++ layers_checked; + const ExPolygons grown = offset_ex(it->second->lslices, min_gap); + for (const ExtrusionEntity *entity : layer->support_fills.flatten().entities) + for (const Slic3r::Polyline &pl : entity->as_polylines()) + for (size_t i = 0; i < pl.points.size(); ++ i) { + // The vertices and the midpoints of the segments between them. + ++ points; + if (contains(grown, pl.points[i])) + ++ too_close; + if (i + 1 < pl.points.size() && contains(grown, (pl.points[i] + pl.points[i + 1]) / 2)) + ++ too_close; + } + } + INFO("support layers checked " << layers_checked << " (unmatched " << layers_unmatched << "), support points " << points + << ", within 0.2 mm of the part " << too_close); + CHECK(layers_checked > 20); + CHECK(layers_unmatched == 0); + REQUIRE(points > 0); + CHECK(too_close == 0); +} + // Two parts along the belt: the second part's slicing frame starts at the belt // below its leading end, so its first layers are empty and interleave with the // first part's printing layers. Those must not reach the G-code as layer changes @@ -1268,6 +1362,9 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ { "machine_start_gcode", "T[initial_tool]\n" }, { "layer_change_gcode", "G92 E0\n" }, }); + // Both export paths drop the empty layers and count the layers the same way. + SECTION("by layer") { config.set_deserialize_strict({{ "print_sequence", "by layer" }}); } + SECTION("by object") { config.set_deserialize_strict({{ "print_sequence", "by object" }}); } Print print; Model model; TriangleMesh cube_a(its_make_cube(20., 20., 20.)); @@ -1282,28 +1379,36 @@ TEST_CASE("Belt G-code has no layer that prints nothing", "[Print][belt][GCode][ const std::string gc = gcode(print); REQUIRE(! gc.empty()); - size_t layers = 0, empty = 0, total_header = 0; + size_t layers = 0, empty = 0, total_header = 0, total_count = 0; bool extruded = true; // before the first layer change - std::istringstream in(gc); - std::string line; auto close_layer = [&]() { if (! extruded) ++ empty; }; - while (std::getline(in, line)) { - if (line.rfind(";LAYER_CHANGE", 0) == 0) { + GCodeReader reader; + reader.apply_config(config); + reader.parse_buffer(gc, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + const std::string &raw = line.raw(); + if (raw.rfind(";LAYER_CHANGE", 0) == 0) { close_layer(); ++ layers; extruded = false; - } else if (line.rfind("; total layer number: ", 0) == 0) { - total_header = size_t(std::atoi(line.c_str() + 22)); - } else if (! extruded && line.rfind("G1 ", 0) == 0 && line.find('E') != std::string::npos - && (line.find('X') != std::string::npos || line.find('Y') != std::string::npos)) { + } else if (raw.rfind("; total layer number: ", 0) == 0) { + // Counted by the G-code processor from the layer changes it saw. + total_header = size_t(std::atoi(raw.c_str() + 22)); + } else if (raw.rfind("; total layers count = ", 0) == 0) { + // GCode::m_layer_count, counted up front from the objects' layers; it also + // drives the M73 progress and the total_layer_count placeholder. + total_count = size_t(std::atoi(raw.c_str() + 23)); + } else if (! extruded && line.extruding(self) && line.dist_XY(self) > EPSILON) { + // Material laid down along a move: a wipe or an unretraction does not count. extruded = true; } - } + }); close_layer(); - INFO("layers " << layers << ", header " << total_header << ", layers without extrusion " << empty); + INFO("layers " << layers << ", header " << total_header << ", count " << total_count + << ", layers without extrusion " << empty); CHECK(layers > 150); // both cubes, 141 layers each, overlapping along the belt CHECK(empty == 0); CHECK(total_header == layers); + CHECK(total_count == layers); } // A part with an overhang on its LEADING side (the end that prints first) needs diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 34f71a71a7..639c36d6c7 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -1140,7 +1140,7 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") // Auto / Mouse ear / Painted collapse to outer-only rather than crashing or // silently producing nothing. const char *brim_type = GENERATE("auto_brim", "brim_ears", "painted", "outer_only", - "inner_only", "outer_and_inner", "no_brim"); + "inner_only", "outer_and_inner", "leading_edge_only", "no_brim"); DYNAMIC_SECTION("brim_type " << brim_type) { DynamicPrintConfig config = belt_brim_config(); config.set_deserialize_strict({ @@ -1157,6 +1157,40 @@ TEST_CASE("Every brim type slices on a belt printer", "[SkirtBrim][belt]") } } +// The leading-edge-only brim is the outer brim cut down to the part's first contact +// with the belt. The cut has to be taken at the first layer with geometry: the slicing +// frame starts at the belt below the footprint, so layers().front() is an empty lead-in +// layer whose contact lies ahead of the part, and a cut taken there left no brim at all. +TEST_CASE("Leading-edge-only brim is laid at the first contact and nowhere else", "[SkirtBrim][belt][Regression]") +{ + auto brim_gcode = [](const char *brim_type) { + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", brim_type }, + { "brim_width", 5 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 0 }, + { "brim_object_gap", 0 }, + }); + return slice({ cube(20) }, config); + }; + const std::string leading = brim_gcode("leading_edge_only"); + const std::string outer = brim_gcode("outer_only"); + + const double brim_z = first_role_z(leading, "brim"); + const double peri_z = first_role_z(leading, "perimeter"); + REQUIRE(brim_z < std::numeric_limits::max()); + REQUIRE(peri_z < std::numeric_limits::max()); + // At the first contact: the brim starts no later than the part does... + CHECK(brim_z <= peri_z + EPSILON); + // ...and stops there, while the outer brim keeps following the footprint. + const int leading_layers = role_layers(leading, "brim"); + const int outer_layers = role_layers(outer, "brim"); + INFO("brim layers: leading-edge " << leading_layers << ", outer " << outer_layers); + CHECK(leading_layers > 0); + CHECK(leading_layers < outer_layers); +} + TEST_CASE("An untilted belt printer gets no brim", "[SkirtBrim][belt]") { // Belt brim needs a tilt to have a belt plane to lie on, and the flat plate brim