diff --git a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp index cf66c4c97e..62d231e375 100644 --- a/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp +++ b/deps_src/libnest2d/include/libnest2d/placers/nfpplacer.hpp @@ -88,6 +88,18 @@ struct NfpPConfig { */ bool explore_holes = false; + /** + * @brief Keep the final pile on the bin. + * + * The final alignment centres the pile on the alignment target. A target + * near an edge (a belt printer starts its parts at the leading end of the + * belt) would push part of a pile that is larger than the room around that + * point off the bed; with this set the pile stops at the edge instead, and a + * pile that does not fit along an axis is centred on it. Off by default, so + * the alignment of every other printer is unchanged. + */ + bool clamp_to_bin = false; + /** * @brief If true, use all CPUs available. Run on a single core otherwise. */ @@ -1113,12 +1125,9 @@ private: 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. - { + // Keep the pile on the bin (see Config::clamp_to_bin). The items' boxes carry + // their inflation, which is the margin left at the edge. + if (config_.clamp_to_bin) { 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; diff --git a/resources/shaders/110/phong.vs b/resources/shaders/110/phong.vs index 10d36e233f..d857147c28 100644 --- a/resources/shaders/110/phong.vs +++ b/resources/shaders/110/phong.vs @@ -7,6 +7,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -46,8 +47,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; if (is_outline) { diff --git a/resources/shaders/140/phong.vs b/resources/shaders/140/phong.vs index c7570edb95..7120b62c66 100644 --- a/resources/shaders/140/phong.vs +++ b/resources/shaders/140/phong.vs @@ -7,6 +7,7 @@ struct SlopeDetection bool actived; float normal_z; mat3 volume_world_normal_matrix; + vec3 up_direction; }; uniform mat4 view_model_matrix; @@ -46,8 +47,8 @@ void main() // Point in homogenous coordinates. world_pos = volume_world_matrix * vec4(v_position, 1.0); - // z component of normal vector in world coordinate used for slope shading - world_normal_z = slope.actived ? (normalize(slope.volume_world_normal_matrix * v_normal)).z : 0.0; + // dot product of world normal with up direction, used for slope shading + world_normal_z = slope.actived ? dot(normalize(slope.volume_world_normal_matrix * v_normal), slope.up_direction) : 0.0; gl_Position = projection_matrix * position; if (is_outline) { diff --git a/src/libslic3r/Arrange.cpp b/src/libslic3r/Arrange.cpp index 8c224af515..79d93f7909 100644 --- a/src/libslic3r/Arrange.cpp +++ b/src/libslic3r/Arrange.cpp @@ -301,10 +301,12 @@ template void fill_config(PConf& pcfg, const ArrangeParams ¶ms) { if (params.is_belt) { - // Pack from the end of the belt that prints first. + // Pack from the end of the belt that prints first, and keep the pile on the + // bed when it is larger than the room around that end. pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT : params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT : PConf::Alignment::BOTTOM_RIGHT; + pcfg.clamp_to_bin = true; } else if (params.is_seq_print) { // Start placing the items from the center of the print bed diff --git a/src/libslic3r/BeltBrim.cpp b/src/libslic3r/BeltBrim.cpp index d77f163467..3f2507f24e 100644 --- a/src/libslic3r/BeltBrim.cpp +++ b/src/libslic3r/BeltBrim.cpp @@ -230,6 +230,19 @@ static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, return poly; } +ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut) +{ + if (region.empty()) + return region; + BoundingBox keep_bb = get_extents(region); + keep_bb.offset(scale_(1.)); + const bool low_side = frame.downhill_sign() < 0; // downhill is -u + const Polygon keep = band_box(keep_bb, frame.from_axis, + low_side ? unscale(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut, + low_side ? u_cut : unscale(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y())); + return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep }); +} + // Everything the per-band line generator needs, gathered once per object. struct BeltBrimContext { @@ -476,20 +489,11 @@ void make_belt_brim(PrintObject &object) width, gap, leading, lateral, bc.frame), bc.frame); - if (bt == btLeadingEdgeOnly && ! bc.region.empty()) { - // Keep only what lies at or downhill of the object's FIRST contact with the - // belt, so the part is supported as it lands and nothing is printed alongside - // it afterwards. The cut is the uphill edge of the first layer's contact band: - // everything past it belongs to later contacts. - const coordf_t u_cut = bc.ctx.cutoff_u(object.layers().front()->print_z); - BoundingBox keep_bb = get_extents(bc.region); - keep_bb.offset(scale_(1.)); - const bool low_side = bc.frame.shear > 0.; // downhill is -u - const Polygon keep = band_box(keep_bb, bc.frame.from_axis, - low_side ? unscale(bc.frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut, - low_side ? u_cut : unscale(bc.frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y())); - bc.region = keep.empty() ? ExPolygons{} : intersection_ex(bc.region, Polygons{ keep }); - } + 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 (bc.region.empty()) return; diff --git a/src/libslic3r/BeltBrim.hpp b/src/libslic3r/BeltBrim.hpp index ac4986c311..3f4fcc97de 100644 --- a/src/libslic3r/BeltBrim.hpp +++ b/src/libslic3r/BeltBrim.hpp @@ -91,6 +91,14 @@ inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &fra // t - not when it is wider in its narrowest Euclidean direction. ExPolygons sweep_ex(const ExPolygons &src, const Point &t); +// "Leading edge only": keep the part of a brim region (unflattened, slicing XY) +// at or downhill of the object's first contact with the belt, so the part is +// supported as it lands and nothing is printed alongside it afterwards. `u_cut` +// is the uphill edge of the first layer's contact band along `frame.from_axis`, +// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side +// `frame.downhill_sign()` points to. +ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut); + // Brim region for one already-flattened belt footprint. All lengths are scaled // and measured in the flattened (true on-belt) metric. // diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index 3b5c5571a6..5fb925f9aa 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -333,6 +333,15 @@ void PrintObject::belt_shift_layer_grid(double delta) for (BeltBrimBand &band : m_belt_brim_prologue) band.print_z += delta; m_slicing_params.belt_floor_z_shift += delta; + // The grid stays shifted across a support-only or brim-only change (posSlice does + // not rerun), so everything slice() derived from it has to follow: the cached floor + // that update_slicing_parameters() restores, and the global offset the organic + // support layers and the adaptive infill octree are placed with. Left alone, the + // next alignment finds a delta of 0 and the floor and the supports sit up to half + // a layer off the grid, unlike a fresh slice. + if (m_belt_floor_z_shift_cache_valid) + m_belt_floor_z_shift_cached += delta; + m_belt_global_z_offset += delta; } // Belt mode: drop layers strictly above z (used to cancel the purge prism early diff --git a/src/libslic3r/BeltTransform.cpp b/src/libslic3r/BeltTransform.cpp index 0a59507ff2..d1ea31d33c 100644 --- a/src/libslic3r/BeltTransform.cpp +++ b/src/libslic3r/BeltTransform.cpp @@ -96,15 +96,13 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) { + if (!has_preslice_remap(config)) + return bb; // Identity remap, or belt mode off. + int pre_rx = int(config.preslice_remap_x.value); int pre_ry = int(config.preslice_remap_y.value); int pre_rz = int(config.preslice_remap_z.value); - if (pre_rx == int(RemapAxis::PosX) && - pre_ry == int(RemapAxis::PosY) && - pre_rz == int(RemapAxis::PosZ)) - return bb; // Identity remap. - auto remap_coord = [](int r, const Vec3d &v) -> double { int axis = r % 3; if (r < 3) return v[axis]; diff --git a/src/libslic3r/BeltTransform.hpp b/src/libslic3r/BeltTransform.hpp index 2fe725e1df..59669fe36a 100644 --- a/src/libslic3r/BeltTransform.hpp +++ b/src/libslic3r/BeltTransform.hpp @@ -47,16 +47,31 @@ class BeltTransformPipeline public: // ---- Identity checks -------------------------------------------------- + // Whether the axis remaps (preslice_remap_* and gcode_remap_*) apply at all. + // The remap fields are only offered in the belt printer group, so a value + // left in a profile must not change a non-belt print: with belt mode off every + // belt-only key is a no-op. This is the one place to widen if a non-belt use + // ever needs them. + static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; } + static bool axis_remap_enabled(const DynamicPrintConfig &config) + { + auto *opt = config.option("belt_printer"); + return opt != nullptr && opt->value; + } + static bool has_preslice_remap(const PrintConfig &config) { - return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) || + return axis_remap_enabled(config) && + (int(config.preslice_remap_x.value) != int(RemapAxis::PosX) || int(config.preslice_remap_y.value) != int(RemapAxis::PosY) || - int(config.preslice_remap_z.value) != int(RemapAxis::PosZ); + int(config.preslice_remap_z.value) != int(RemapAxis::PosZ)); } // Overload accepting DynamicPrintConfig (used in static slicing_parameters). static bool has_preslice_remap(const DynamicPrintConfig &config) { + if (! axis_remap_enabled(config)) + return false; auto get_int = [&](const char *key) -> int { auto *opt = config.option>(key); return opt ? int(opt->value) : 0; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 7fefed0f87..690c4e99ac 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -107,6 +107,7 @@ #include "calib.hpp" #include "libslic3r_version.h" #include "GCode/BeltKinematics.hpp" +#include "FirstLayerPlane.hpp" // Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332. // We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB. #if ! defined(TBB_VERSION_MAJOR) @@ -3133,15 +3134,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato 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 - // the identity mapping (0,1,2) — so a reused writer never retains a stale - // non-identity mapping from a prior export. has_axis_remap() returns false at - // identity, so identity/default output stays unchanged. + // G-code axis remap. Only belt printers get one (see + // BeltTransformPipeline::axis_remap_enabled): a remap left in a profile must + // not change a non-belt print. Sync the writer's remap state to the current + // export UNCONDITIONALLY — even at the identity mapping (0,1,2) — so a reused + // writer never retains a stale non-identity mapping from a prior export. + // has_axis_remap() returns false at identity, so identity/default output stays + // unchanged. { - int rx = int(print.config().gcode_remap_x.value); - int ry = int(print.config().gcode_remap_y.value); - int rz = int(print.config().gcode_remap_z.value); + const bool remap = BeltTransformPipeline::axis_remap_enabled(print.config()); + int rx = remap ? int(print.config().gcode_remap_x.value) : int(RemapAxis::PosX); + int ry = remap ? int(print.config().gcode_remap_y.value) : int(RemapAxis::PosY); + int rz = remap ? int(print.config().gcode_remap_z.value) : int(RemapAxis::PosZ); m_writer.set_axis_remap(rx, ry, rz); BoundingBoxf bbox_bed(print.config().printable_area.values); m_writer.set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), @@ -3153,13 +3157,21 @@ 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()); - // 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. + // Belt writers only: travel-speed selection becomes per-point (see + // GCodeWriter::uses_pointwise_travel_speed()), which must not change for + // non-belt printers. The writer gets the same test the extrusions use, so a + // travel is judged against the belt surface (belt_height_above_floor) exactly + // like the path it leads to, and not against the FirstLayerPlane, which + // misreports the height under a non-identity gcode_remap_*. Writer points + // carry the G-code origin and extruder offset that point_to_gcode() added; + // the belt surface is described in the object's own frame. if (print.config().belt_printer.value) { - m_writer.set_first_layer_plane( - m_first_layer_plane.get(), - print.config().initial_layer_print_height.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(), + point_logical.z())); + }); } // How many times will be change_layer() called? @@ -6295,7 +6307,11 @@ LayerResult GCode::process_layer( // fall back to the legacy `!first_layer` predicate so behavior is // bit-identical to the pre-feature path. bool past_first_layer_band = !first_layer; - if (m_first_layer_plane && m_first_layer_plane->is_active()) { + if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0) { + // Belt surface known for this object: measured from the belt itself, as the + // extrusions and travels are, rather than through FirstLayerPlane. + past_first_layer_band = past > 0; + } else if (m_first_layer_plane && m_first_layer_plane->is_active()) { past_first_layer_band = false; if (object_layer != nullptr) { // Conservatively walk the layer's lslice bboxes; if every bbox's @@ -10617,6 +10633,36 @@ std::string GCode::set_object_info(Print *print) { return gcode.str(); } +// Whether an object layer lies entirely past the first-layer band above the belt: +// 1 when its lowest point is at least one band thickness above the belt, 0 when +// any of it is inside the band, -1 when the belt surface is not known for this +// layer (not a belt print, an explicit first-layer plane, or no object layer), in +// which case the caller falls back to FirstLayerPlane / the slicing layer index. +int GCode::belt_layer_past_first_layer_band(const Layer *object_layer) const +{ + if (object_layer == nullptr) + return -1; + // The belt surface is linear in the sliced XY, so a bbox's lowest point above + // it is at one of its corners. + double min_height = std::numeric_limits::max(); + bool known = false; + for (const BoundingBox &bb : object_layer->lslices_bboxes) { + const double xs[2] = { unscale(bb.min.x()), unscale(bb.max.x()) }; + const double ys[2] = { unscale(bb.min.y()), unscale(bb.max.y()) }; + for (double x : xs) + for (double y : ys) { + double h; + if (! this->belt_height_above_floor(Vec3d(x, y, object_layer->print_z), h)) + return -1; + known = true; + min_height = std::min(min_height, h); + } + } + if (! known) + return -1; + return min_height >= this->first_layer_band_mm() - EPSILON ? 1 : 0; +} + 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 diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 3a08ffccec..c5268705c3 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -993,6 +993,9 @@ protected: // 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; + // 1 / 0 / -1: the object layer is entirely past the first-layer band above the + // belt / reaches into it / the belt surface is not known for it. + int belt_layer_past_first_layer_band(const Layer *object_layer) const; int layer_id() const { if (m_layer == nullptr) return -1; diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 1832e24a84..fa418e1b62 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3214,6 +3214,33 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config) { m_parser.apply_config(config); + // Belt printer: remember the file's belt keys for export_config_for_render(). The + // config block lists belt_printer for every printer, so a non-belt file loaded while + // a belt printer is selected switches the preview's belt view off, and a belt file + // loaded on another printer brings its own tilt, remaps and bed along. + m_belt_render_config.clear(); + { + const auto *belt = config.option("belt_printer"); + if (belt != nullptr) { + static const char *belt_keys[] = { + "belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global", + "belt_preslice_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global", + "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform", + "belt_frame_tilt_decouple", "belt_frame_tilt_angle", + }; + for (const char *key : belt_keys) + if (const ConfigOption *opt = config.option(key); opt != nullptr) + m_belt_render_config.set_key_value(key, opt->clone()); + // The Rev remaps mirror inside the build volume, so the designed view needs + // the bed the file was sliced for. Only a belt file may override it. + static const char *bed_keys[] = { "printable_area", "printable_height" }; + if (belt->value) + for (const char *key : bed_keys) + if (const ConfigOption *opt = config.option(key); opt != nullptr) + m_belt_render_config.set_key_value(key, opt->clone()); + } + } + //BBS const ConfigOptionFloatsNullable* nozzle_volume = config.option("nozzle_volume"); if (nozzle_volume != nullptr) { @@ -3738,6 +3765,7 @@ DynamicConfig GCodeProcessor::export_config_for_render() const config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values)); config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values)); config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values)); + config.apply(m_belt_render_config); return config; } diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 09dd99816e..a5c5ce7914 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -1105,6 +1105,10 @@ class Print; private: CommandProcessor m_command_processor; GCodeReader m_parser; + // Belt printer: the belt keys of the loaded file's config block (plus the bed they + // are relative to), handed to the preview through export_config_for_render() so the + // belt view and its back-transform follow the file, not the selected printer. + DynamicConfig m_belt_render_config; EUnits m_units; EPositioningType m_global_positioning_type; EPositioningType m_e_local_positioning_type; diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp index a6e81dfe1e..6176b6693c 100644 --- a/src/libslic3r/GCodeWriter.cpp +++ b/src/libslic3r/GCodeWriter.cpp @@ -1,5 +1,4 @@ #include "GCodeWriter.hpp" -#include "FirstLayerPlane.hpp" #include "Config.hpp" #include "Extruder.hpp" #include "Geometry.hpp" @@ -57,8 +56,8 @@ bool GCodeWriter::must_skip_lift_now() const 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); + if (m_first_layer_point_test) + return m_first_layer_point_test(point_logical); return m_is_first_layer; } diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp index 224ad4c1e4..88be72a6f1 100644 --- a/src/libslic3r/GCodeWriter.hpp +++ b/src/libslic3r/GCodeWriter.hpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "Extruder.hpp" #include "Point.hpp" #include "Polygon.hpp" @@ -20,7 +22,6 @@ namespace Slic3r { -class FirstLayerPlane; class GCodeWriter { public: @@ -168,13 +169,13 @@ public: 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; } + // Per-point first-layer test. When set, travel speed selection asks it per + // destination point (in the writer's logical placed frame) instead of using + // the layer-coarse m_is_first_layer flag. GCode installs it on belt printers + // with the same test its extrusions use (GCode::on_first_layer(point)), so a + // travel is judged against the belt surface exactly as the path it leads to. + using FirstLayerPointTest = std::function; + void set_first_layer_point_test(FirstLayerPointTest test) { m_first_layer_point_test = std::move(test); } // 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 @@ -197,9 +198,8 @@ protected: std::string _travel_to_z(double z, const std::string &comment); - // 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. + // Whether a destination gets first-layer treatment. With a point test + // installed it decides; otherwise the layer-coarse 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 @@ -207,17 +207,15 @@ protected: 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 - // 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 + // layer. Set for writers that install a first-layer point test. 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 point-test-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; } + bool uses_pointwise_travel_speed() const { return bool(m_first_layer_point_test); } - // Borrowed; null = inactive. - const FirstLayerPlane *m_first_layer_plane = nullptr; - double m_first_layer_thickness_mm = 0.; + FirstLayerPointTest m_first_layer_point_test; bool m_force_normal_lift = false; // The machine frame mapping. Owns the axis-remap state that used to live diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 417b472bd0..748bed4a1f 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1777,7 +1777,10 @@ bool PrintObject::invalidate_step(PrintObjectStep step) invalidated |= this->invalidate_steps({ posIroning, posContouring, posSimplifyInfill }); invalidated |= m_print->invalidate_steps({ psSkirtBrim }); } else if (step == posSlice) { - invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill }); + // 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 }); 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 8795cdc444..b384a28ef3 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -895,6 +895,15 @@ void PrintObject::slice() this->update_layer_height_profile(*this->model_object(), m_slicing_params, layer_height_profile); m_print->throw_if_canceled(); m_typed_slices = false; + // The belt state below is only written while belt mode is on (and the min-Z + // lift only when there is a rotation or remap). Start every slice from zero, + // or a project switched from a belt printer to a normal one, or whose tilt + // axis was set to None, keeps the previous offsets: the adaptive infill octree + // and the organic support layers (PrintObject.cpp) would still be shifted by + // 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(); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 0d1c141fa8..9c4a03ca47 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -353,6 +353,31 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con bool is_object_config = (!is_global_config && !is_plate_config); + // Belt printer: a raft and a draft shield are refused by Print::validate(), and + // the fields that would clear them are greyed out in belt mode, so a preset that + // carries either could not be sliced at all. Reset them instead of only disabling + // the fields. + if (GUI::wxGetApp().preset_bundle != nullptr) { + const auto *belt_opt = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + const auto *raft_opt = config->option("raft_layers"); + const auto *shield_opt = config->option>("draft_shield"); + const bool has_raft = raft_opt != nullptr && raft_opt->value > 0; + const bool has_shield = shield_opt != nullptr && shield_opt->value != dsDisabled; + if (belt_opt != nullptr && belt_opt->value && (has_raft || has_shield)) { + const wxString msg_text = _(L("Raft and draft shield are not available on belt printers.\nThey have been disabled.")); + MessageDialog dialog(m_msg_dlg_parent, msg_text, "", wxICON_WARNING | wxOK); + DynamicPrintConfig new_conf = *config; + is_msg_dlg_already_exist = true; + dialog.ShowModal(); + if (has_raft) + new_conf.set_key_value("raft_layers", new ConfigOptionInt(0)); + if (has_shield) + new_conf.set_key_value("draft_shield", new ConfigOptionEnum(dsDisabled)); + apply(config, &new_conf); + is_msg_dlg_already_exist = false; + } + } + // layer_height shouldn't be equal to zero auto layer_height = config->opt_float("layer_height"); if (layer_height < EPSILON) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index cec7f9507e..b03e6b5f03 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -1376,9 +1376,12 @@ 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. 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; + // menu item) from the G-code itself. Only BeltGCode writes the belt header, so its tilt + // (gcode_result.belt_tilt_angle, abs of the slicing rotation) says whether this is belt + // G-code; the selected printer does not, for a file opened from disk. The back-transform + // below still reads print.config(), which Plater::load_gcode() fills from the file's own + // config block (GCodeProcessor::export_config_for_render). + m_belt_view_enabled = gcode_result.belt_tilt_angle > 0.f; m_belt_angle_deg = gcode_result.belt_tilt_angle; const bool current_top_layer_only = m_viewer.is_top_layer_only_view_range(); @@ -1394,7 +1397,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // On a belt printer the toolpath geometry fed to libvgcode also depends on the // designed/raw view state (the back-transform is applied in convert), so the // same result is converted again only when that view has been toggled. - const bool same_belt_view = !print.config().belt_printer.value || m_last_belt_show_designed == m_belt_show_designed; + const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed; if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) { //BBS: add logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result); @@ -1441,7 +1444,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const // the toolpath geometry into model/Cartesian space using the general belt // inverse (handles any mesh rotation + shear + axis remap). When off, the raw // machine-frame G-code is shown (useful for debugging the transform itself). - const bool is_belt = print.config().belt_printer.value; + const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value; Transform3d belt_inv = (is_belt && m_belt_show_designed) ? compute_belt_back_transform(print.config()) : Transform3d::Identity(); // Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's diff --git a/tests/fff_print/test_gcode_processor.cpp b/tests/fff_print/test_gcode_processor.cpp index 2d3aab2909..d45d6534f7 100644 --- a/tests/fff_print/test_gcode_processor.cpp +++ b/tests/fff_print/test_gcode_processor.cpp @@ -6,6 +6,7 @@ #include "test_utils.hpp" +#include #include #include #include @@ -47,3 +48,48 @@ TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcesso "; 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)); } + +TEST_CASE("Non-belt start G-code moves keep the first-layer Z in the processor", "[GCodeProcessor]") +{ + // The belt path (GCodeWriter tests: "start-gcode prepare-stage moves keep their real Z") + // stores the real Z of a move inside the start G-code. Every other printer must keep + // the historical behaviour: a prepare-stage move is pinned to the first-layer height + // so the preview does not draw the start sequence's travel. The gate is the belt + // header, so a file without one, whatever its config block says, takes this path. + struct BBLPrinterGuard { + bool prev = GCodeProcessor::s_IsBBLPrinter; + BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = false; } + ~BBLPrinterGuard() { GCodeProcessor::s_IsBBLPrinter = prev; } + } bbl_guard; + + const std::string gcode = + "G90\n" + "G21\n" + "M83\n" + ";TYPE:Custom\n" + "G1 E-1.5 F2100\n" + "G1 X45 Y0.3 Z50 F12000\n" // prepare-stage travel to a high Z + "G1 E1.5 F1800\n" + ";TYPE:Outer wall\n" + "G1 X46 Y0.3 Z50 E0.05\n" + "; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n"; + + GCodeProcessor processor; + FullPrintConfig config; + config.initial_layer_print_height.value = 0.3; + processor.apply_config(config); + processor.process_buffer(gcode); + const GCodeProcessorResult &result = processor.get_result(); + REQUIRE_THAT(result.belt_tilt_angle, WithinAbs(0., 1e-6)); + + size_t first_extrude = result.moves.size(); + for (size_t i = 0; i < result.moves.size(); ++i) + if (result.moves[i].type == EMoveType::Extrude) { first_extrude = i; break; } + REQUIRE(first_extrude < result.moves.size()); + REQUIRE(first_extrude > 0); + + // The extrusion keeps its real Z; the prepare-stage move before it is pinned to the + // first-layer height. + CHECK_THAT(result.moves[first_extrude].position.z(), WithinAbs(50., 1e-3)); + CHECK_THAT(result.moves[first_extrude - 1].position.z(), WithinAbs(0.3, 1e-3)); +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index a098a4a653..bdebf052b6 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -14,6 +14,7 @@ #include #include #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/libslic3r.h" #include #include "libslic3r/Surface.hpp" #include "libslic3r/Config.hpp" @@ -26,6 +27,10 @@ #include "libslic3r/Print.hpp" #include "libslic3r/Layer.hpp" +#include "libslic3r/BuildVolume.hpp" +#include "libslic3r/Support/TreeModelVolumes.hpp" +#include "libslic3r/Support/TreeSupportCommon.hpp" +#include "libslic3r/Polygon.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/GCodeReader.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" @@ -969,3 +974,284 @@ TEST_CASE("Slicing errors are reported per object with the object's name", "[Pri CHECK(message.rfind("floating cube: ", 0) == 0); CHECK(message.find("empty first layer") != std::string::npos); } + +// --------------------------------------------------------------------------- +// Belt mode must be invisible when it is off, and must not leave traces behind. +// --------------------------------------------------------------------------- + +// Everything the slicer decided, without the lines that legitimately differ between +// two exports of the same print: comments (the config block lists every key, the +// header carries the export time) and the thumbnail blocks. +static std::string gcode_body(const std::string &gcode) +{ + std::string body; + std::istringstream in(gcode); + for (std::string line; std::getline(in, line); ) { + line.erase(std::min(line.size(), line.find(';'))); + while (! line.empty() && line.back() == ' ') + line.pop_back(); + if (! line.empty()) + body += line + '\n'; + } + return body; +} + +static DynamicPrintConfig belt_test_config() +{ + 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 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + return config; +} + +TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged", "[Print][belt][Regression]") +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "z_hop", 0 }, + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + const std::string reference = gcode_body(slice({ TestMesh::overhang }, config)); + REQUIRE(! reference.empty()); + + // Every belt key a profile can carry, at a value that would change a belt print. + // belt_printer stays off, so none of them may reach the G-code: the axis remaps are + // gated on belt mode, the rest is only read on belt printers. build_plate_tilt_x/y + // and an explicit first_layer_plane are features of their own on a flat bed and are + // left alone here; "leading_edge_only" prints as an outer brim by design. + config.set_deserialize_strict({ + { "belt_printer", 0 }, + { "belt_printer_infinite_y", 0 }, + { "belt_slice_rotation", "y" }, + { "belt_slice_rotation_angle", 30 }, + { "belt_slice_rotation_global", 0 }, + { "belt_preslice_global", 0 }, + { "preslice_remap_x", "pos_x" }, + { "preslice_remap_y", "pos_z" }, + { "preslice_remap_z", "neg_y" }, + { "preslice_remap_global", 1 }, + { "gcode_remap_x", "rev_x" }, + { "gcode_remap_y", "pos_z" }, + { "gcode_remap_z", "pos_y" }, + { "gcode_back_transform", 0 }, + { "belt_frame_tilt_decouple", 1 }, + { "belt_frame_tilt_angle", 30 }, + { "first_layer_plane_offset", 1 }, + { "first_layer_plane_thickness", 1 }, + { "belt_support_floor_offset", -5 }, + { "belt_support_floor_mode", "none" }, + { "belt_support_z_offset_mode", "raft_only" }, + { "enable_belt_purge_tower", 1 }, + { "belt_purge_tower_width", 10 }, + { "leading_brim_length", 10 }, + { "extra_brim_width", 5 }, + }); + CHECK(gcode_body(slice({ TestMesh::overhang }, config)) == reference); +} + +TEST_CASE("Switching a sliced project from belt to non-belt matches a fresh slice", "[Print][belt][Regression]") +{ + // The organic support layers and the adaptive infill octree are placed with the + // belt global Z offset, and the mesh with the belt min-Z lift. Both are only + // written while belt mode slices, so they used to survive a switch away from it. + DynamicPrintConfig flat = DynamicPrintConfig::full_print_config(); + flat.set_deserialize_strict({ + { "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" }, + { "sparse_infill_pattern", "adaptivecubic" }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + DynamicPrintConfig belt = belt_test_config(); + belt.set_deserialize_strict({ + { "enable_support", 1 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + { "sparse_infill_pattern", "adaptivecubic" }, + }); + + // Both prints are placed with the belt config, so only the slicing history differs. + auto fresh_slice = [&](const DynamicPrintConfig &target) { + Print print; + Model model; + init_print({ TestMesh::overhang }, print, model, belt); + print.apply(model, target); + const std::string out = gcode(print); + return gcode_body(out); + }; + auto resliced = [&](const DynamicPrintConfig &target) { + Print print; + Model model; + init_print({ TestMesh::overhang }, print, model, belt); + REQUIRE(! gcode(print).empty()); + print.apply(model, target); + const std::string out = gcode(print); + return gcode_body(out); + }; + SECTION("belt printer to a flat-bed printer") { + CHECK(resliced(flat) == fresh_slice(flat)); + } + SECTION("belt tilt axis set to None") { + DynamicPrintConfig untilted = belt; + untilted.set_deserialize_strict({ { "belt_slice_rotation", "none" } }); + CHECK(resliced(untilted) == fresh_slice(untilted)); + } +} + +TEST_CASE("A support-only change on a belt purge print matches a fresh slice", "[Print][belt][PurgeTower][Regression]") +{ + // Snapping the purge prism onto the parts' layer grid shifts every object's layers by + // up to half a layer. A support-only change reruns support generation without + // reslicing, so the cached belt floor and the global Z offset have to carry the + // snap too, or the supports land on the pre-snap grid. + auto make_config = [](bool support) { + DynamicPrintConfig config = multifilament_config(2, { + { "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_belt_purge_tower", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" }, + { "layer_change_gcode", "G92 E0\n" }, + }); + config.set_deserialize_strict({ + { "enable_support", support ? 1 : 0 }, + { "support_type", "tree(auto)" }, + { "support_style", "organic" }, + }); + return config; + }; + const std::vector> overrides { + { { "extruder", 1 } }, { { "extruder", 2 } }, + }; + auto build = [&](Print &print, Model &model, const DynamicPrintConfig &config) { + init_print(std::vector{ mesh(TestMesh::overhang), cube(20) }, print, model, config, &overrides); + model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + REQUIRE(print.has_belt_purge_tower()); + }; + + std::string fresh; + { + Print print; + Model model; + build(print, model, make_config(true)); + fresh = gcode_body(gcode(print)); + } + REQUIRE(! fresh.empty()); + + Print print; + Model model; + build(print, model, make_config(false)); + REQUIRE(! gcode(print).empty()); + // Support only: posSlice stays valid, posSupportMaterial reruns. + print.apply(model, make_config(true)); + CHECK(gcode_body(gcode(print)) == fresh); +} + +TEST_CASE("Organic tree supports place a support blocker at its own height above a raft", "[Print][Support][Regression]") +{ + // TreeModelVolumes consumes the support blockers in the same index space as the + // layer outlines, where object layer i sits at num_raft_layers + i, but + // slice_support_blockers() returns them in object-layer space. Without the shift + // every blocker lands num_raft_layers too low, so branches are kept out of the + // wrong layers and may pass through the blocked ones. + 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" }, + { "raft_layers", 3 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + // A blocker floating beside the cube, 8 mm to 12 mm above the bed, so a collision at + // its centre can only come from the blocker itself (the part keeps its mesh + // coordinates in object space, hence the offset relative to the part). + ModelObject *object = model.objects.front(); + ModelVolume *blocker = object->add_volume(TriangleMesh(its_make_cube(6., 6., 4.))); + blocker->set_type(ModelVolumeType::SUPPORT_BLOCKER); + const Vec3d part_offset = object->volumes.front()->get_offset(); + blocker->set_offset(Vec3d(part_offset.x() + 20., part_offset.y(), 10.)); + print.apply(model, config); + print.set_status_silent(); + print.process(); + + const PrintObject &print_object = *print.objects().front(); + const std::vector bed = { { 0., 0. }, { 200., 0. }, { 200., 200. }, { 0., 200. } }; + const BuildVolume build_volume{ bed, print.config().printable_height.value, {}, {} }; + TreeSupport3D::TreeModelVolumes volumes{ print_object, build_volume, scaled(1.), scaled(0.5), 0, {} }; + + // The generator's raft layer count: the raft itself plus the gap layers up to the object. + const size_t num_raft = TreeSupport3D::TreeSupportSettings(TreeSupport3D::TreeSupportMeshGroupSettings(print_object), + print_object.slicing_parameters()).raft_layers.size(); + REQUIRE(num_raft >= 3); + // Object layers the blocker was sliced into (object-layer space, as the generator + // receives them). + const std::vector blockers = print_object.slice_support_blockers(); + size_t first = 0, last = 0; + bool found = false; + for (size_t i = 0; i < blockers.size(); ++ i) + if (! blockers[i].empty()) { + if (! found) { first = i; found = true; } + last = i; + } + REQUIRE(found); + REQUIRE(last - first > num_raft); + // The blocker's centre in the slicing frame (add_volume centred its mesh on its offset). + const Vec3d centre3 = print_object.trafo_sliced() * blocker->get_offset(); + const Point centre = Point::new_scale(centre3.x(), centre3.y()); + auto collides = [&](size_t tree_layer) { + for (const Polygon &poly : volumes.getCollision(0, TreeSupport3D::LayerIndex(tree_layer), false)) + if (poly.contains(centre)) + return true; + return false; + }; + // In TreeModelVolumes' index space the blocker lives at num_raft + object layer. + CHECK(collides(num_raft + first)); + CHECK(collides(num_raft + last)); + // The layers just below it, where an unshifted blocker would land, are free; the + // layers just above the unshifted range, which the blocker does occupy, are not. + CHECK_FALSE(collides(first)); + CHECK_FALSE(collides(first + num_raft - 1)); + CHECK(collides(last + 1)); + CHECK(collides(last + num_raft)); +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 537aac8528..45086b9d2a 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -834,6 +834,60 @@ TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][b CHECK(belt_tools_for_role(gc, "brim") == std::set{ 0 }); // filament 1 -> tool 0 } +// Number of brim segments the belt brim generator produced for `object`: the lattice +// lines of every per-layer band plus the apron prologue. Each segment is written as one +// extruding move, so this is what a G-code count has to match. A pass count cannot see a +// band emitted twice back to back (two copies of the same band merge into one pass). +static long belt_brim_segments(const PrintObject &object) +{ + auto segments = [](const ExtrusionEntityCollection &fills) { + long n = 0; + for (const ExtrusionEntity *entity : fills.flatten().entities) + for (const Polyline &pl : entity->as_polylines()) + n += long(pl.size()) - 1; + return n; + }; + long n = 0; + for (const ExtrusionEntityCollection &band : object.belt_brim_by_layer()) + n += segments(band); + for (const BeltBrimBand &band : object.belt_brim_prologue()) + n += segments(band.fills); + return n; +} + +// Number of extruding moves in the G-code whose role is `role`. +static long role_segments(const std::string &gcode, const std::string &role) +{ + long n = 0; + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.dist_XY(self) > EPSILON && line.comment().find(role) != std::string_view::npos) + ++ n; + }); + return n; +} + +TEST_CASE("Belt brim writes every generated segment exactly once", "[SkirtBrim][belt]") +{ + // The pass count above cannot tell one band from the same band twice in a row; the + // segment count can, so a brim band emitted twice back to back fails here. + DynamicPrintConfig config = belt_brim_config(); + config.set_deserialize_strict({ + { "brim_type", "outer_only" }, + { "brim_width", 4 }, + { "brim_object_gap", 0 }, + { "leading_brim_length", 6 }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + const std::string gc = gcode(print); + + const long expected = belt_brim_segments(*print.objects().front()); + REQUIRE(expected > 100); + CHECK(role_segments(gc, "brim") == expected); +} + // B - multi extruder (wall filament id 2). Every belt-brim line must print on the object's // wall filament (index 2 -> tool 1), and the total number of passes must equal the // single-extruder baseline: no per-filament doubling. diff --git a/tests/libslic3r/test_arrange.cpp b/tests/libslic3r/test_arrange.cpp index d8c675954a..f4dad02808 100644 --- a/tests/libslic3r/test_arrange.cpp +++ b/tests/libslic3r/test_arrange.cpp @@ -279,12 +279,15 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]") // 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]") +TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange][belt]") { 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); + params.is_belt = true; + params.belt_axis = 1; + params.belt_tilt_slope = 1.f; arrange(items, belt, params); @@ -300,6 +303,29 @@ TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]") require_no_overlap(items); } +// The clamp is a belt feature. Printers whose best_object_pos is off-centre (the A1 mini +// and the H2 family) keep their final alignment: the pile is centred on that point, even +// when that puts part of it outside the bed. +TEST_CASE("Arrange leaves the final alignment of a flat bed unclamped", "[Arrange]") +{ + const BoundingBox bed_ = bed(95, 500); + ArrangePolygons items = squares(4, 90.); + ArrangeParams params = quiet_params(scaled(2.)); + params.align_center = Vec2d(0.5, 0.05); + + arrange(items, bed_, params); + + BoundingBox pile; + for (const ArrangePolygon &ap : items) { + REQUIRE(ap.bed_idx == 0); + pile.merge(ap.transformed_poly().contour.bounding_box()); + } + // Centred on the 5% mark of the bed's length, not pushed inside it. + CHECK_THAT(unscaled(pile.center().y()), Catch::Matchers::WithinAbs(0.05 * 500., 15.)); + CHECK(pile.min.y() < 0); + 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 diff --git a/tests/libslic3r/test_belt_brim.cpp b/tests/libslic3r/test_belt_brim.cpp index 05c650a809..f6d794f939 100644 --- a/tests/libslic3r/test_belt_brim.cpp +++ b/tests/libslic3r/test_belt_brim.cpp @@ -324,11 +324,9 @@ SCENARIO("belt_brim_region builds an inner ring inside a hole", "[BeltBrim]") { } SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[BeltBrim]") { - // BeltBrim.cpp ~445-458 clips the region to the object's first-contact band and keeps - // only what lies at or downhill of it. That clip is built with band_box(), which is - // file-static, so the rectangular half-band is reconstructed here with the SAME sign - // rule the code uses (low_side = shear > 0, i.e. downhill is -u) to pin the convention - // for both tilt signs. downhill_sign() is the exported accessor the flag mirrors. + // The production clip, belt_brim_clip_leading_edge(), keeps what lies at or downhill + // of the first-contact cut. downhill_sign() pins the convention for both tilt signs: + // low_side = shear > 0, i.e. downhill is -u. const coord_t mm = scale_(1.); const double shear = GENERATE(1.0, -1.0); DYNAMIC_SECTION("shear " << shear) { @@ -336,27 +334,37 @@ SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[Bel CHECK((frame.downhill_sign() < 0) == (frame.shear > 0.)); const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; // straddles the cut - const coord_t u_cut = 8 * mm; - const BoundingBox bb = get_extents(region); + const coordf_t u_cut = 8.; // mm - const bool low_side = frame.shear > 0.; - const coord_t lo = low_side ? bb.min.y() : u_cut; - const coord_t hi = low_side ? u_cut : bb.max.y(); - Polygon keep; - keep.points = { Point(bb.min.x(), lo), Point(bb.max.x(), lo), - Point(bb.max.x(), hi), Point(bb.min.x(), hi) }; - const ExPolygons kept = intersection_ex(region, Polygons{ keep }); + const ExPolygons kept = belt_brim_clip_leading_edge(region, frame, u_cut); REQUIRE(! kept.empty()); const BoundingBox kb = get_extents(kept); if (frame.shear > 0.) { // downhill is -u: nothing above the cut survives. - CHECK(kb.max.y() <= u_cut + 2); - CHECK(kb.min.y() < u_cut); + CHECK(kb.max.y() <= 8 * mm + 2); + CHECK(kb.min.y() < 8 * mm); } else { // downhill is +u: nothing below the cut survives. - CHECK(kb.min.y() >= u_cut - 2); - CHECK(kb.max.y() > u_cut); + CHECK(kb.min.y() >= 8 * mm - 2); + CHECK(kb.max.y() > 8 * mm); + } + // Half of the box is kept either way, and the full width across the belt. + CHECK_THAT(area(kept), Catch::Matchers::WithinRel(area(region) * (frame.shear > 0. ? 8. / 20. : 12. / 20.), 0.01)); + CHECK(kb.min.x() == 0); + CHECK(kb.max.x() == 20 * mm); + } + WHEN("the cut lies beyond the region") { + const BeltBrimFrame frame { 1.0, 1 }; + const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; + THEN("a cut past the uphill end keeps everything") { + CHECK_THAT(area(belt_brim_clip_leading_edge(region, frame, 30.)), Catch::Matchers::WithinRel(area(region), 0.001)); + } + THEN("a cut before the downhill end keeps nothing") { + CHECK(belt_brim_clip_leading_edge(region, frame, -5.).empty()); + } + THEN("an empty region stays empty") { + CHECK(belt_brim_clip_leading_edge(ExPolygons{}, frame, 8.).empty()); } } }