# Belt Printing Bug Fixes & Feature Updates
This should be the majority of substantive work keeping ``belt-printer``
from being ready to merge into ``main``. It includes Hanif Koh's review
fixes from #15685 and the answers to his review on #14394, findings from
running the branch on a BabyBelt Pro and an IR3 V2, crash fixes
contributed by Unlayered3D, and arrange and purge-tower changes for
multi-colour belt prints.
The merge of current `main` into this branch is prepared and tested
locally. The conflicts are in the acceleration refactor of
`GCode::_extrude`, the ClipperLib namespace clean-up and a few test
files.
Tested with `libslic3r_tests`, `fff_print_tests` and `libnest2d_tests`
on Linux, validated on a stock Klipper BabyBelt Pro.
## New features
**Belt arrangement.** Parts of the same colour are grouped along the
belt into a single print run. Packing starts at the end that prints
first, following the slicing rotation and the sign of the angle. Arrange
reserves the purge prism's strip and the brim width along the bed edges,
then regenerates the prism from the result instead of moving it as a
part. Piles aimed at an off-centre `best_object_pos` are clamped to the
bed. Grouping uses a soft cost: if the belt is too short for separate
runs, colours overlap rather than move to another plate.
**Purge tower sizing.** The prism stops at the plate end. The purge
planner's existing warning reports what a shortened bar can't absorb. A
brim is accepted next to the purge tower again because the purge plan's
layer-grid shift now also moves the brim's apron bands.
**First-layer fan band.** On a belt, "the first layers" are a band along
the belt rather than the first slicing layers. The generator marks where
each extrusion enters and leaves the band. The cooling buffer keeps the
fan off inside it on every layer, taking precedence over overhang and
bridge fan requests.
Thanks to:
@Unlayered3D, @shubhracc, @dlc60, @Rexit
**Profiles.** Z-hop defaults to 0 on belt printer bases and belt
filaments; it can be turned back on. Axis remap options are shown only
in Develop mode. IdeaFormer, Printcepts and Custom bundle versions are
bumped.
Thanks to: @RobMink, @Rexit
## Bug fixes
- Scarf joint seams no longer start below the layer on a belt.
Previously, each seam caused a 0.28 mm belt back-step into the previous
layer ("the belt jumped backwards and the head hit the part"). — credit:
@dlc60
- The CLI no longer rejects every belt print with -102. The
printable-height check compared machine Z, which is belt travel on a
belt printer.
- The belt header is written outside the optional file header block, so
printers with a BTT TFT thumbnail still get belt view in the preview. —
credit: BabyBelt Discord
- The dormant tilted-bed rendering is removed from Prepare view; the bed
is shown as the slicing pipeline treats it. — credit: HanifKoh
- Plate icons, number and name no longer run across the neighbouring
plate on a long, narrow bed; their scale is bounded by the gap between
plates.
- Modifiers and support blockers no longer extend a belt object's sliced
range. The `is_model_part` filter had gone missing with some debug
logging. — credit: HanifKoh
- Crossing-perimeter avoidance no longer dereferences a null layer in
either pass while travelling on a brim apron layer. — credit:
Unlayered3D
- 3MF files with non-finite vertex coordinates are rejected instead of
crashing qhull during load. — credit: Unlayered3D
- The CLI no longer crashes on a project without `printable_height` or
with fewer filaments than were loaded. — credit: Unlayered3D
- The island tour cache is keyed on the island layout, preventing
out-of-bounds reads on later layers with fewer islands. — credit:
Unlayered3D
- The top/bottom painting projection no longer erases from an empty
vector when no shell layers are requested. — credit: Unlayered3D
- Belt purge planning detects filament changes by scanning the tool
ordering instead of checking the first layer's flag, which a brim apron
layer never carries. — credit: Unlayered3D
- The purge prism never gets a brim, regardless of its config. — credit:
Unlayered3D
- Containment tests treat the plate as open along Y on an infinite-Y
belt printer. — credit: Unlayered3D
- Belt brim lattice lines close to the belt move uphill; narrow bands no
longer get near-duplicate lines.
- Organic supports that reach the belt slice without negative flow.
- Hanif Koh's review items: restored the gantry clearance check in
`Print::validate`, read the pre-slice remap header at its real length,
removed unused `clip_support_fills()` and the two unimplemented support
floor modes (legacy values map to `none`), dropped the per-extrusion
transform determinant, indexed apron layers into the first layer's
nozzle map, read the brim axis from the config, removed tagged
diagnostic logging and planning-doc references, and documented the
exclude-object frame. — credit: Hanif Koh
- Hanif Koh's fixes from #15685 include the plate offset in the belt
writer, painted supports and seams under the belt transform, shared
build-plate tilt helpers, the belt header as the source of the tilt,
brim band loop and filament, and G-code export invalidation. — credit:
hanifkoh
At this time there are no known issues with belt printing nor any known
regressions in non-belt-printing execution paths. I have been using
these builds for all of my printing for several months now and have had
no issues.
all_paths_inside() accepts the path bounding box only within 3*EPSILON of the
bed and otherwise tests every move; a belt print's moves are machine-frame
coordinates whose Z is belt travel, so that test can never pass, and the
designed view's min-corner anchor leaves the box a fraction of a millimetre
below zero. Every multi-object belt plate therefore reported a path beyond the
plate. The belt preview now judges the back-transformed box with a millimetre
of room.
ensure_belt_purge_tower only ever looked at the current plate and kept a single
prism, so the other plates had no tower and switching plates moved the one
prism around. Every plate is now planned on its own: a prism that lies on no
plate is stale, a plate whose prism matches its recorded inputs is left alone,
the rest are deleted and recreated, highest index first.
The plate's icons, number and name scale with the plate's depth, but they sit in
the gap to the next plate, which scales with its width. On a long, narrow bed
(a 95 x 500 mm belt) they came out 40 mm wide and ran across the neighbouring
plate. The scale is now also bounded by the gap, which leaves ordinary beds
unchanged.
Plater::set_bed_shape read the belt keys from the plater's own config, which
never carries them, so the branch that tilted the bed model, drew the slicing
arrow and plane and switched the build volume to belt mode never ran. The
Prepare view shows the bed as the slicing pipeline treats it, flat; the
gravity arrow from build_plate_tilt stays, as does the preview's belt view,
which takes its angle from the G-code header.
The G-code viewer takes the belt tilt only from the belt header comments, but
they were written inside the header block that is left out when a BTT TFT
thumbnail is configured, so such a printer never got belt view. The comments
are not part of the header block; they go after it, and after the thumbnails
that firmware needs first.
The purge plan snaps every object onto one layer grid after the brim is built;
the per-layer brim bands follow their layers but the apron bands below the
first layer carry their own print_z and were left behind, which is why a brim
was refused next to a purge tower object. The shift now moves them too and the
combination is accepted again.
On a belt the parts print in belt order, so every colour change between parts
is a filament change. Arrange packs items in extruder order already, but it
grew the pile around its centre, so the colours ended up interleaved. A belt
print now packs from the leading end of the bed, each row filling across the
belt before the pile advances, and the objective charges an item for every
packed part of another colour it does not fully follow along the belt,
counting the tilted layers that reach cot(angle) * height past a part, so
each colour prints as one run. The direction follows the slicing rotation: a
rotation about X prints toward +Y, one about Y toward -X, and a negative angle
flips it. Packing from the edge also means the brim has to be kept on the
bed: a belt brim is printed brim_width wide for every brim type, so that much
is reserved along every edge (between parts the brims may overlap, as on any
printer).
The purge prism is regenerated from the arranged parts, flush with the far
edge of the bed, yet arrange moved it about like a part and packed parts into
the strip it comes back to. Arrange now skips the prism and reserves its strip
with a fixed virtual item, like a bed exclusion area, whenever the parts use
more than one filament.
The prism's length follows the parts plus a ramp per unit of height; with the
height at its cap that ran 100 mm past the end of a 500 mm belt and the project
could not print. The bar now stops at the plate end, and the purge planner's
existing warning reports what the shortened bar cannot absorb.
The band was a second pass over the finished layer that fought the fan commands
the layer pass had already written (overhang, bridge and resume requests). The
generator now marks where each segment enters and leaves the band and the layer
pass treats the band as the strongest fan request, so there is one place that
decides the fan.
The purge plan moves objects onto a common layer grid after the brim bands are
built, so the two cannot share a print. The prime tower setting alone still does
not block a brim. The missing-prism warning now counts the filaments the objects
use, as the GUI does.
With best_object_pos away from the bed centre the placer packs the pile
inside the bin and then translates it so its centre lands on that point,
without checking that it still fits there. A belt printer aims at the
leading end of the belt (BabyBelt Pro: 0.5, 0.05), so any pile longer than
the 50 mm around that point was pushed past the edge: four 90 mm parts on
the 95 x 500 mm belt ended with one across the edge and one outside while
290 mm of belt stayed free.
The final alignment now stops the pile at the edge of the bin; the items'
inflated boxes leave the object spacing as the margin. A pile that does not
fit along an axis is centred on it, as before.
A tilted layer runs from the belt to the top of the part, so a wall loop
that starts above the belt still passes along it. Tagging only the path's
first point left such loops out of the band entirely; the band is now
evaluated at each segment, with the tag capped where the fan stops
depending on it.
The clearance test needs the relative-E reset in its layer change G-code to
get past validate()'s other checks, and now asserts the height message. The
fan band test counts cycles rather than commands: the band is decided per
path start, so a cube cycles the fan far less often than a benchy.
Covers the case from Hanif Koh's review of #14394 (belt raft layers below
the object with no lower bound), which the negative-Z bottom layer fix in
layer_initialize() addresses.
With a first layer of about 0.28 mm or more at 45 degrees (or a shallower
belt) the brim band is wider than one bead and its lines go on the nominal
lattice. A lattice line could land where the belt is almost at the band's
print_z; its flow was clamped to half a layer while the nozzle sat nearly on
the belt. Such a line now moves uphill to the 0.75 fraction the single-line
case uses, and a line that lands on the previous one is skipped.
Ported from the Unlayered fork (patch 0007 of its belt port series, found
there by fuzzing first layer heights). The fork's companion fix, restricting
the brim filament to those the writer was handed (0008), is not needed here:
ToolOrdering registers the brim filament on every band's layer, so the writer
always has it. A test pins that with every object a flush target.
EXCLUDE_OBJECT_DEFINE keeps plate coordinates on a belt printer: the frame
after the slicing rotation is undone and before the G-code axis remap and
machine-frame shear, which is where the object stands on the belt.
Raised in Hanif Koh's review of #14394.
The purge tower is a model object the GUI creates and sizes, and libslic3r
only purges into one that exists. A multi-filament belt project sliced from
the CLI without it changed filament with nowhere to purge, silently.
Raised in Hanif Koh's review of #14394.
The cooling buffer's band pass rebuilt positions from the layer's G-code
and tested them against the first-layer plane. The G-code is in machine
coordinates and the plane is in slicing coordinates, so on the shipped
profiles the nearest move was over 100 mm from a 0.2 mm band and the pass
never changed the fan. GCode::_extrude() already knows each path's height
above the belt, so it now tags the band changes and the buffer applies and
strips the tags.
The pass also took the S of every M106 as the part fan, whatever its P
index, and stored that 0..255 value where a percentage was expected (an
auxiliary fan line came back as M106 S651); it now uses FanMover's parser,
which ignores other fans, and converts to percent. It no longer overwrites
the layer's intended speed, only the fan's actual state.
Raised in Hanif Koh's review of #14394.
belt_brim_instances_compatible() runs while the slicing parameters can be
stale, like the rest of the brim predicates, which read the print config.
Raised in Hanif Koh's review of #14394.
Apron bands looked up their filament and nozzle config slot with a running
counter, while object layers use Layer::id(), so band N read the map of
object layer N. They precede layer 0 and now use its assignment.
Raised in Hanif Koh's review of #14394.
The mesh transform is a rotation and an axis permutation, so its
determinant is always 1; rebuilding the forward transform on every
extrusion to divide the flow by it changed nothing.
Raised in Hanif Koh's review of #14394.
enable_prime_tower stays on for any multi-filament project, but a belt
printer never prints the classic tower and the belt purge prism is an
ordinary object that never takes a brim, so every brim on a multi-filament
belt print was refused for nothing.
Raised in Hanif Koh's review of #14394.
validate() skipped the build-volume height check whenever the machine-frame
transform was active, which is every shipped belt profile, so a 400 mm
object passed on a 300 mm printable_height. The transform only changes how
the height is written to G-code; the clearance check from f682ab5cd3
applies regardless.
Raised in Hanif Koh's review of #14394.
Drops the [BELT-DEBUG], [BELTRACE], [BELT-CALIB] and [BELT-PREVIEW] log
lines, the SLIC3R_BELT_DIAGNOSTIC_LOG blocks, and the counters and
temporaries that existed only to feed them. Six of the purge tower lines
logged at warning level, which is Orca's default, on every plan. Raised in
Hanif Koh's review of #14394.
preslice_remap_*, preslice_remap_global and gcode_remap_* describe the
printer's kinematics and are set once by its profile. A wrong value sends
the gantry outside the machine (a user preset with the pre-slice remap in
place of the G-code remap emitted gantry moves to Y=646 mm), so they are no
longer offered in Expert mode.
On a belt printer a lift is a move along the belt axis (0.4 mm / sin 45 =
0.57 mm of belt travel out and back on every hop), not a lift away from the
part. The three belt printer bases now ship z_hop 0, the IR3 V2 leaf no
longer restates 0.4, and the BabyBelt Pro and IR3 V2 filaments stop
overriding the printer with filament_z_hop 0.4. The option stays editable.
PartPlate's containment tests treat the plate as open along Y on a belt printer with
belt_printer_infinite_y, so a long part is no longer flagged outside the plate in Prepare
while the slicer and the G-code checks accept it. The check reads the printer preset
through the app object, which does not exist headlessly, so it is guarded on the plater.
The prism's generator already sets no_brim; PrintObject::has_belt_brim() now also ignores
any brim setting on the prism (belt_purge_tower_object), so a brim on the parts beside it
never blocks purging.
ToolOrdering::has_wipe_tower() reads the first layer's flag. On a belt the first layer may
be a brim apron band, which carries neither object nor support and never gets the flag, so
with a brim the purge plan returned early and nothing was purged. Scan the layers for a
change.
With top_shell_layers = 0 the `top` vector is never filled and erasing its begin() was
undefined (found by fuzzing on a painted object dropped below the plate).
The per-filament island tour was cached by island centroids only. A later layer with the
same centroids but fewer islands (thin walls, negative volumes) reused the stale visit
list, whose catch-all index pointed past the layer's islands, and extrude_perimeters read
freed memory (three fuzz crashes, planar and belt). The per-instance island layout is part
of the cache key and the use site never indexes past the islands.
Found by fuzzing the headless slicer:
- A BBS-style 3MF without Metadata/project_settings.config segfaulted the CLI silently on
the missing printable_height option.
- A project saved with fewer filaments (or filament groups) than --load-filaments overran
the filament variant tables (segfault in the variant match) and then hit an uncaught
ConfigurationError from set_with_restore_2 (std::terminate). The tables are regenerated
for the filaments the project did not know about, the destination vectors grown first
(only from a non-empty source), the match bounded, and a failure becomes a CLI config
error.
A 3MF vertex with a nan/inf coordinate was accepted by both parsers and crashed qhull in
ModelVolume's convex hull while the file was still loading. Both vertex handlers refuse it,
and volume generation checks again whichever parser produced the geometry. The main
parser's _stop_object_xml_parser keeps a message a handler already set.
The default 0.4 mm z-hop is a 0.57 mm move along the belt axis and its
return tripped the back-step check. Shipped belt profiles print without a
z-hop, so the test does too.
check_multi_extruder_gcode_valid() compares each object's max Z with
printable_height. On a belt printer machine Z is belt travel (a 3DBenchy
on the BabyBelt Pro runs from Z=197 to Z=309 on a 69 mm printable_height),
so every belt export set the over-height error bit and the CLI refused the
plate with -102 "G-code in unprintable area". The preview already skips its
ToolHeightOutside warning for the same reason; the export check now does
too. The XY printable-area check is unchanged.
The header tags lost their belt_ prefix in the Part 3.2 rename (20
characters now), but the parser still skipped 25, so every axis read as
pos_x. Found in Hanif Koh's review of #14394.
A scarf joint begins one layer height below the current layer and ramps
up along the wall. On a tilted belt that start is a step backwards along
the belt axis, into the previous layer's wall at the seam: 0.283 mm per
0.2 mm layer at 45 degrees. With an aligned seam the nozzle rams the same
spot on every layer. A BabyBelt Pro benchy with seam_slope_type=external
showed 601 such back-steps from layer 107 on, and in the field the belt
"jumped backwards" and the head knocked the part loose.
Belt printers now skip the scarf in GCode::extrude_loop, and the process
tab greys the scarf controls out for them, as it already does for arc
fitting. The regression test slices a cube on a belt with the scarf
enabled and checks the belt axis never steps back by a layer pitch.
Brings in upstream/belt-printer (the Sept 14 main merge) plus Hanif Koh's
21 review-fix commits from PR #15685, on top of the MachineKinematics
refactor and the purge-prism / tree-support / first-layer-speed fixes.
Conflict resolution:
- BeltGCodeWriter is gone (kinematics refactor), so Hanif's plate-offset
fix for it is ported into GCodeWriter: the first-layer-plane checks in
travel_to_xy / travel_to_xyz / _travel_to_z now evaluate the plate-local
point, and BeltGCode::init_belt_writer hands the stored plate origin to
the writer it installs.
- init_belt_writer(Print&) takes Hanif's signature; the BBL flag is set on
the surviving writer by GCode::_do_export.
- The shared emit_belt_brim_bands() loop keeps the BeltFloorObjectGuard the
local branch added, so apron bands classify first-layer height against
their own object.
- eager_lift keeps effective_type: it now carries set_force_normal_lift().
- GCodeWriter's initializer list follows Hanif's member order with
m_kinematics in its declared position.
- TreeSupport::detect_overhangs uses Hanif's clamped build_plate_tilt_slope()
for the non-belt path and the belt shear for the belt path.
The lift, speed and cached-extruder members now live in the protected section ahead of the private ones; list their initializers first so the list reads in construction order. No behaviour change.