Belt printer: address the 2026-10-07 review of #14394 (#16277)

## Description

Addresses every item of @raistlin7447's review of 2026-10-07 on #14394
(https://github.com/OrcaSlicer/OrcaSlicer/pull/14394#pullrequestreview-5447529307),
one commit per item, plus a follow-up commit from a second adversarial
pass over the result.

**Organic supports (the one non-belt difference raistlin's export
fixtures found).** The debug-strip commit dda58b07cd had deleted the
loop in `organic_draw_branches()` that trims every branch slice against
the collision volume, the bed and the belt plane. It is restored exactly
as on `main` (plus the belt-floor clip). New test: a cube carrying a 60
mm plate, organic supports, flat-bed printer; on every support layer no
support extrusion may come within 0.2 mm of the part's slice. To be
clear about what it proves: it guards that invariant, but on this
fixture the loop's own effect is a sub-millimetre reshaping of one
branch (checked by running the test with the loop compiled out), so the
test does not by itself fail without the loop. The loop's effect is
shown separately by slicing six organic fixtures with the stripped and
the restored binary (CLI): on a plate-over-cube fixture the stripped
build brings a branch to 0.02 mm from the part's slice at the cube's
corner where the restored build keeps 0.39 mm; the Bulbasaur project
differs in ~2000 support lines; a fixture with no wall near the branches
is byte-identical.

**G-code (belt only).**
- First-layer speed test: the writer passes points with the plate origin
already removed, so only the instance part of `m_origin` is subtracted
now.
- The mixed-filament sub-layer pass calls `on_set_origin()` like the
main instance loop.
- `m_belt_in_band` is reset per object in by-object printing, with the
cooling buffer.
- `m_layer_count` counts only the layers that are written, through the
same predicate `collect_layers_to_print()` uses
(`belt_object_layer_prints_something()`); the by-object overload drops
the empty belt layers as well, so both print sequences write the same
layer changes. The empty-layers test now runs for both sequences and
checks `; total layers count` too. Side effect worth knowing: with the
empty entries dropped per object, a multi-filament belt layer no longer
selects a filament it then prints nothing with. On belt_project.3mf (two
filaments, belt purge tower) the T commands go from 472 to 106 with the
extruded length per filament unchanged; every removed tool change was
followed by no extrusion.

**Invalidation / ordering.**
- `posSlice` now also invalidates `posDetectOverhangsForLift` (not
belt-gated: a re-slice starts the layers over with empty overhang
regions while the step stayed done; this makes an incremental re-slice
match a fresh slice).
- `btLeadingEdgeOnly` takes part in the layer-0 outer-wall-first rule
and the matching `brim_type` → `posPerimeters` rule (not belt-gated:
`Print.cpp` already prints it as an outer brim on a flat bed).
- Adding or removing an object invalidates the support step of the other
belt-brim owners, so their brims are clipped against what is on the
plate now.

**Belt brim (found during the GUI pass, pre-existing since #16236).**
"Leading edge only" produced no brim at all: the cut that narrows the
outer brim to the first contact was taken at `layers().front()`, which
since the lead-in change is an empty layer whose contact lies ahead of
the part, so the whole region was clipped away. The cut is now taken at
the first layer with geometry; `leading_edge_only` joins the
all-brim-types test and a new test checks the brim starts no later than
the part and covers fewer layers than the outer brim.

**UI.** Build plate tilt X/Y are read-only on a belt printer (they are
derived from the belt tilt). The belt temperature tower refuses a range
without an embossed model, before the project is replaced, instead of
falling back to the 230–190 model.

**Strings, dead code, comments.** Tooltip and comment say cot and
1/|sin| (what `MachineFrameTransform.cpp` does); `gcode_remap_*` labels
and tooltips are `L("literal")` so they are extracted; removed
`belt_remapped_bbox()`, `belt_min_z()`, `m_belt_global_xy_correction`,
`LayerTools::has_belt_brim`, the `belt_surface_z` constant, and (second
pass) the unused kinematics inverse (`to_logical`,
`apply_axis_remap_inverse`, `to_build_volume` and their state), the
`world_coordinates()`, `is_active()` and `belt_brim_areas_by_layer()`
accessors and two unused overloads; rewrote the comments that still
described removed code (BeltBrim.cpp SEQUENCING, GCodeWriter.hpp,
calib.cpp/hpp, GCode.hpp, BeltSliceStrategy, PrintObjectSlice.cpp,
PrintApply.cpp).

Not changed, noted for a follow-up: the outer-wall-first rule keys on
numeric layer 0, which on a belt is usually an empty lead-in layer, so
the part's first contact layer does not get the rule; and a
leading-length-only brim (zero base width) is excluded by the
`brim_width > 0` test. Both need a geometry-based rule rather than a
one-line change.

## Screenshots/Recordings/Graphs

Build plate tilt fields greyed out on a belt printer, the temperature
tower error dialog, and the brim before/after deleting a neighbouring
object are attached below (from the Xvfb GUI pass).

## Tests

- `fff_print_tests`: all cases pass (includes the new organic test and
the extended empty-layers test in both print sequences);
`libslic3r_tests` pass.
- Organic test run with the loop compiled out (temporary local switch):
passes either way on this fixture, see above; the CLI comparison on six
fixtures is where the loop's effect is visible.
- `OrcaSlicer_profile_validator -s` on the belt vendors and Prusa as
control; `scripts/orca_profile_tool.py check`; profile tool unit tests
(281).
- `scripts/clang_tidy_diff.py` against `belt-printer`: clean.
- GUI pass on Xvfb (Linux): tilt fields greyed/editable with belt
on/off; temperature tower error for 250–200 leaves the project
untouched, 230–190 loads the tower; multi-colour demo by layer 595
slider layers = 595 layer changes with matching labels and no greying
while dragging; two cubes by object 314 = 314; outer brim complete after
deleting the neighbouring cube; organic supports clear of the part on
the belt preset and on a flat-bed variant; raw G-code toggle via menu
and `B` keeps the slider index; no crash or assert in the logs. The
leading-edge brim finding from this pass is fixed above.

OS: Linux (Ubuntu), GCC, local build. Written with AI assistance (Claude
Code), every change reviewed and tested locally as listed.

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