Belt printer: address the review on #14394 (#16195)

Follow-up to #14394, addressing @raistlin7447's review (review
5421968464) item by item, plus the tests it asked for.

## Review items

1. **Stale belt offsets after switching printers** —
`PrintObject::slice()` now zeroes `m_belt_min_z`,
`m_belt_global_z_offset` and `m_belt_global_xy_correction` before
slicing. They were only written in belt mode, so a project switched to a
normal printer (or whose tilt axis was set to None) kept the old
offsets, which shifted the adaptive infill octree and the organic
support layers.
2. **Blocker indexing in `TreeModelVolumes`** — a test now pins the
index the support blockers land on with a raft (object layer + raft
layers), including the layers just below and just above where an
unshifted blocker would sit.
3. **Arrange clamp** — the final-alignment clamp in libnest2d is opt-in
(`NfpPConfig::clamp_to_bin`) and arrange sets it for belt printers only.
Printers with an off-centre `best_object_pos` (A1 mini, H2 family) keep
their alignment; a flat-bed test pins that and the existing clamp test
is now a belt test.
4. **Belt view from the file, not the preset** —
`GCodeProcessor::apply_config(DynamicPrintConfig)` carries the file's
belt keys (and, for a belt file, its
`printable_area`/`printable_height`, which the Rev remaps need) into
`export_config_for_render()`; `GCodeViewer` enables the belt view from
the header tilt. A normal `.gcode` opened with a belt printer selected
is no longer back-transformed, and a belt file opened on another printer
brings its own tilt and remaps.
5. **Purge-prism snap vs. support-only changes** —
`belt_shift_layer_grid()` also shifts `m_belt_floor_z_shift_cached` and
`m_belt_global_z_offset`, so the restored floor and the organic support
layers follow the snapped grid.
6. **Raft / draft shield on a belt** — `update_print_fff_config()`
resets `raft_layers` and `draft_shield` with the usual warning dialog
instead of only greying out the fields `Print::validate()` rejects.
7. **First-layer travel speed and second-layer temperature** —
`GCodeWriter` takes a first-layer point test instead of the
`FirstLayerPlane`; `GCode` installs one that goes through
`on_first_layer(point)` (the belt surface, as the extrusions use),
converting the writer's logical point back to the object frame.
`past_first_layer_band` uses a new `belt_layer_past_first_layer_band()`
on the same basis. The `FirstLayerPlane` path is kept for an explicit
XY/YZ/XZ choice or a non-zero plane offset, as before.
8. **Leading-edge brim test** — `belt_brim_clip_leading_edge()` is
exported and called by both the generator and the test (which also
checks the kept area and the cut-beyond-region cases).
9. **phong.vs** — both `110/phong.vs` and `140/phong.vs` get
`up_direction` and the `dot()` slope test, so studio lighting and
realistic phong highlight overhangs with the tilt.

## Remap gating

`preslice_remap_*` and `gcode_remap_*` are gated on `belt_printer`
through one helper, `BeltTransformPipeline::axis_remap_enabled()`. The
fields are only offered in the belt group, so a value left in a profile
must not change a non-belt print. That helper is the one place to widen
if a non-belt use ever needs them.

## Tests (as requested)

- Belt-only keys at non-default values leave non-belt G-code unchanged.
- Switching a sliced project from belt to non-belt (and to tilt axis
None) matches a fresh slice.
- A support-only change on a belt purge print matches a fresh slice.
- Non-belt start G-code moves keep the first-layer Z in the processor.
- The belt brim's segment count (not pass count) catches a band emitted
twice back to back.

## One fix outside belt code

The belt-to-non-belt test exposed a gap that `main` shares:
`PrintObject::invalidate_step(posSlice)` re-invalidates
`posSupportMaterial` but not `posSimplifySupportPath`
(`invalidate_steps()` does not propagate), so after any re-slice the
regenerated support paths were exported unsimplified — extra vertices
and tiny `E.00001` moves. `posSimplifySupportPath` is now in that list;
with it the re-sliced and fresh outputs match byte for byte (comments
aside).

## Verification

- `libslic3r_tests`: 1116 passed, 2 skipped. `fff_print_tests`: 351
passed (561 427 assertions). Built on Linux with GCC against OCCT 8.0.1
deps.
- `scripts/clang_tidy_diff.py -p build-tidy --base eb5b9a77b9`: no
findings.
- The GUI files (`ConfigManipulation.cpp`, `GCodeViewer.cpp`) compile;
the preview change was not exercised interactively.
This commit is contained in:
Joseph Robertson
2026-10-05 23:19:41 -05:00
committed by GitHub
24 changed files with 680 additions and 95 deletions
@@ -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;
+3 -2
View File
@@ -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) {
+3 -2
View File
@@ -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) {
+3 -1
View File
@@ -301,10 +301,12 @@ template<class PConf>
void fill_config(PConf& pcfg, const ArrangeParams &params) {
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
+18 -14
View File
@@ -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 &region, 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<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
low_side ? u_cut : unscale<double>(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<double>(bc.frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
low_side ? u_cut : unscale<double>(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;
+8
View File
@@ -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 &region, 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.
//
+9
View File
@@ -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
+3 -5
View File
@@ -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];
+17 -2
View File
@@ -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<ConfigOptionBool>("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<ConfigOptionEnum<RemapAxis>>(key);
return opt ? int(opt->value) : 0;
+61 -15
View File
@@ -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<FirstLayerPlane>(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<double>::max();
bool known = false;
for (const BoundingBox &bb : object_layer->lslices_bboxes) {
const double xs[2] = { unscale<double>(bb.min.x()), unscale<double>(bb.max.x()) };
const double ys[2] = { unscale<double>(bb.min.y()), unscale<double>(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
+3
View File
@@ -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;
+28
View File
@@ -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<ConfigOptionBool>("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<ConfigOptionFloatsNullable>("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;
}
+4
View File
@@ -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;
+2 -3
View File
@@ -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;
}
+18 -20
View File
@@ -10,6 +10,8 @@
#include <string>
#include <charconv>
#include <vector>
#include <functional>
#include <utility>
#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<MachineKinematics> 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<bool(const Vec3d &point_logical)>;
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
+4 -1
View File
@@ -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.
+9
View File
@@ -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();
+25
View File
@@ -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<ConfigOptionBool>("belt_printer");
const auto *raft_opt = config->option<ConfigOptionInt>("raft_layers");
const auto *shield_opt = config->option<ConfigOptionEnum<DraftShield>>("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<DraftShield>(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)
+8 -5
View File
@@ -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
+46
View File
@@ -6,6 +6,7 @@
#include "test_utils.hpp"
#include <cstddef>
#include <fstream>
#include <string>
#include <catch2/catch_test_macros.hpp>
@@ -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));
}
+286
View File
@@ -14,6 +14,7 @@
#include <catch2/generators/catch_generators_range.hpp>
#include <catch2/catch_message.hpp>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include <cstddef>
#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<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> overrides {
{ { "extruder", 1 } }, { { "extruder", 2 } },
};
auto build = [&](Print &print, Model &model, const DynamicPrintConfig &config) {
init_print(std::vector<TriangleMesh>{ 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<Vec2d> 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<coord_t>(1.), scaled<coord_t>(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<Polygons> 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));
}
+54
View File
@@ -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<int>{ 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.
+27 -1
View File
@@ -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<double>(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
+26 -18
View File
@@ -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());
}
}
}