The green highlight and tint no longer cover a colour preview, and return while a stroke is
painted. The Fast view also keeps the other parts of a multi-part object.
The new Mixed colors setting caps how many mixed filaments a bake adds. They are picked from the
texture's colours, and only the ones the bake paints with are created. Previewing no longer creates
filament slots, both previews show a mix in its slot's colour, and the bake paints each mix with the
slot it actually got.
A `<metadata>` element without a `type` attribute makes
`get_attribute()` return nullptr, which was then assigned to a
`std::string` and read as a C string.
Check it the way the sibling metadata handler already does, and stop the
parse.
Regression test in `tests/libslic3r/test_amf.cpp`, a new file: the suite
had no AMF test at all.
# Belt printing for OrcaSlicer
This merges the `belt-printer` branch into `main`. It adds support for
conveyor-belt ("infinite Z") printers like the BabyBelt Pro, the
IdeaFormer IR3 V2 and the CR-30 family, along with the things that had
to grow around it: tilted-plane slicing, supports that terminate on the
belt, belt-aware brims, a purge tower that works without a flat bed,
multi-colour belt prints, a preview that shows the part the way it was
designed, and starter profiles.
It has been a long road (the first attempt was #12733 back in March, the
pipeline has been rebuilt twice since) and a lot of people have put work
into it. Credits are at the bottom; please tell me if I've missed
anyone.
Closes#2628, closes#11344, closes#6885, closes#9004, closes#14188.
---
## How belt printing works
Slicers assume the bed sits on the XY plane and layers stack along Z. A
belt printer breaks that in two ways at once: the belt is tilted
(usually 45°) and the axis that advances the belt is the printer's Z, so
the "bed" lives on the XZ plane and is, in principle, infinitely long.
<img width="6360" height="2702" alt="A flat bed vs a tilted belt"
src="https://github.com/user-attachments/assets/3f5542f2-6dab-42bf-9233-f96d863b40c8"
/>
Rather than teach every part of the slicer about tilted beds, the branch
leaves the slicing engine alone and transforms what goes in and what
comes out. The pipeline has five steps.

<details>
<summary><b>The five steps in detail</b></summary>
### 1. Pre-slice rotation
The mesh is rotated by the belt angle before slicing
(`BeltSliceStrategy`, `BeltTransform`), so that the ordinary horizontal
slicer produces layers that are actually tilted planes relative to the
part. Rotation is about X by default (belt along Y); Y rotation and a Z
lift are supported too. A "global" mode rotates every object about one
common origin, which is what keeps several parts on one belt consistent
with each other.
### 2. Slice
Nothing special. The mesh is rotated, the slicer does what it always
does. This is the reason most of Orca's features (walls, infill, seams,
ironing, painting, …) just work on a belt without belt-specific code.
### 3. Supports
Supports are generated after slicing against a virtual floor: the belt
surface, expressed in the rotated frame (`BeltFloorContext`). Normal,
tree and organic supports all terminate on that plane instead of on Z=0,
and nothing may be generated below it. Painted supports and seams are
transformed with the same `trafo_sliced()` as the layers.

### 4. G-code back-transform
The G-code is rotated back into the model's Cartesian frame
(`BeltBackTransform`). A nice side effect: with step 5 switched off you
can slice at a non-standard angle and print the result on a normal
printer. The first belt-sliced Benchy was printed exactly that way, on a
Sovol SV08.
### 5. Machine frame
The printer doesn't know its bed is tilted. To make straight walls come
out straight, the axes are remapped (the default mapping is X → reversed
X, Y → Z, Z → Y) and the result is sheared and scaled:
```math
\begin{bmatrix} X \\ Y \\ Z \end{bmatrix}
\longrightarrow
\begin{bmatrix} X \\ \dfrac{Y}{\cos\alpha} \\ Z + Y\cdot\tan\alpha \end{bmatrix}
```
This lives in `GCodeWriter` behind a small `MachineKinematics` strategy
(`BeltKinematics` on belts, identity otherwise), so the writer itself
has one code path. The slicing angle and the machine angle can differ if
you want to, e.g. slice at 30° on a 45° machine — mind your nozzle
clearance if you do.
</details>
A short recording of the back-transform from the original PR:
https://github.com/user-attachments/assets/cdb9cc83-711d-48b7-9d86-a014a32c5e8e
---
## What had to change to make it work
Belt mode is gated on the `belt_printer` printer setting; with it off,
every code path below is the old one.
<details>
<summary><b>Slicing and geometry</b></summary>
- `PrintObjectSlice` / `PrintObject`: the rotation, Z lift and
per-object layer-grid shift, plus the invalidation that goes with them.
Modifiers and painted volumes are transformed with the same matrix as
the model.
- `FirstLayerPlane`: "the first layer" on a belt is a band along the
belt, not the first slicing layer. First-layer speed, line width and the
fan band are measured against it.
- `Print::validate`: clearance checks against the gantry instead of the
printable height; skirt, raft, draft shield, the classic prime tower,
arc fitting, spiral vase with brim, and scarf-joint seams are refused or
disabled on belts because each of them either doesn't exist on a belt or
moves the belt the wrong way (a scarf seam starts one layer low, which
on a belt is a 0.28 mm back-step into the previous layer at every seam).
- Z-hop defaults to 0 on belt profiles; a lift on a belt is a belt move.
</details>
<details>
<summary><b>Supports</b></summary>
- `SupportMaterial`, `TreeSupport`, `TreeSupport3D`, `TreeModelVolumes`:
a shared `BeltFloorContext` provides the belt plane; supports clip to
it, extension layers are numbered sequentially, and the first-layer
flange that used to be stamped under everything is gone.
- `build_plate_tilt_x/y` (from #12733) is now derived from the slicing
rotation in `Print::apply`, so GUI and CLI agree; it's capped below 90°.
- Organic supports that reach the belt no longer produce negative flow.
</details>
<details>
<summary><b>Brims (#15155)</b></summary>
A belt first layer is effectively a single line of contact, so a brim
matters more than usual. `BeltBrim` generates per-layer bands along the
belt plus an "apron" ahead of the part, in the object's brim filament.
Two belt-specific settings came with it: **Leading edge brim length**
(more lines on the side printed first) and **Extra brim width** (across
the belt), plus a **Leading edge only** brim type.
<img width="1849" height="1043" alt="belt brim"
src="https://github.com/user-attachments/assets/f963ed8e-53e7-48f8-a495-123cb9ae27f7"
/>
</details>
<details>
<summary><b>Multi-colour: the belt purge tower</b></summary>
The classic wipe tower can't exist on a belt (its G-code bypasses the
transform and it needs a flat bed to stand on). Instead the GUI
generates a long, thin "purge prism" beside the parts, flush with the
far edge of the belt, one per plate (`BeltPurgeTower.cpp`). It is a real
model object, so it is sliced like everything else, and
`Print::_plan_belt_purge` routes every filament change into it via
`flush_into_objects`. It's sized from the flush matrix, split into one
island per simultaneous tool change, snapped onto the parts' layer grid,
cut off after the last colour change and stripped of infill no change
claimed, so what prints is a good deal smaller than the model you see in
Prepare.
</details>
<details>
<summary><b>G-code generation and cooling</b></summary>
- `GCodeWriter` + `MachineKinematics`/`BeltKinematics`: the
back-transform, axis remap, shear and scale, lifts that are belt moves,
and the first-layer travel speed.
- `GCode.cpp` / `BeltGCode`: a belt header (slicing rotation, remaps,
machine tilt) that the preview reads back; it is written outside the
optional header block so printers with a BTT TFT thumbnail still get it.
Exclude-object outlines are emitted in the plate frame.
- `CoolingBuffer`: the "first layers" the fan stays off for are a band
above the belt, marked per extrusion segment by the generator
(`;_BELT_BAND_START/END`) and honoured on every layer.
- `GCodeProcessor`: belt header parsing, start-G-code Z handling, and
height checks that don't compare belt travel against the printable
height.
- `ToolOrdering` / `BeltPurge`: filament changes are detected by
scanning the ordering (an apron layer never carries the first-layer
flag).
</details>
<details>
<summary><b>GUI</b></summary>
- Printer settings tab: the belt group (slicing rotation, angle, global
mode, infinite Y, purge tower, floor settings). The axis remap and
pre-slice remap options are Develop-mode only.
- `ConfigManipulation`: everything that doesn't apply on a belt is
greyed out (skirt, raft, draft shield, the wipe tower group, scarf
seams, …).
- Preview: a "designed view" that back-transforms the toolpaths onto the
model so you see the part upright, with `B` toggling the raw
machine-frame G-code (`GCodeViewer`, `Shortcuts`). The tilt comes from
the belt header, so imported G-code behaves.
- Arrange: parts are packed from the end of the belt that prints first,
colours are grouped into runs so each filament change happens once, the
purge tower's strip and the brim width are reserved, and piles aimed at
an off-centre `best_object_pos` are clamped to the bed (`Arrange.cpp`,
`ArrangeJob.cpp`, one clamp in `libnest2d`).
- `PartPlate`: plate icons stay in the gap between plates on a long,
narrow bed; the plate is open along Y for containment tests on an
infinite-Y belt.
- Calibration: a belt temperature tower (overhang variant) that slices
correctly on a tilt.
- The old tilted-bed rendering in Prepare was dormant and has been
removed; the bed is shown as the slicing pipeline treats it.
</details>
<details>
<summary><b>Config options</b></summary>
Printer: `belt_printer`, `belt_printer_infinite_y`,
`belt_slice_rotation`, `belt_slice_rotation_angle`,
`belt_slice_rotation_global`, `belt_preslice_global`,
`belt_frame_tilt_decouple`, `belt_frame_tilt_angle`,
`belt_support_floor_mode`, `belt_support_floor_offset`,
`belt_support_z_offset_mode`, `enable_belt_purge_tower`,
`first_layer_plane`, `first_layer_plane_offset`,
`first_layer_plane_thickness`, `build_plate_tilt_x/y`,
`gcode_back_transform`, `gcode_remap_x/y/z`, `preslice_remap_x/y/z`,
`preslice_remap_global`.
Process: `belt_purge_tower_width`, `leading_brim_length`,
`extra_brim_width`, brim type `leading_edge_only`. Object:
`belt_purge_tower_object`.
All of them have defaults that leave non-belt printers untouched, and
old `belt_support_floor_mode` values map to `none`.
</details>
<details>
<summary><b>Tests</b></summary>
`tests/libslic3r/test_belt_brim.cpp` and `test_arrange.cpp`, and belt
cases in `fff_print` (`test_print.cpp`, `test_skirt_brim.cpp`,
`test_gcodewriter.cpp`, `test_gcode_processor.cpp`): scarf gate, fan
band, gantry clearance, organic supports on the belt, brim with and
without the purge tower, apron widths, machine mapping at non-45°
angles, first-travel lift, start-G-code Z, arrange clamp and colour
grouping. All three suites pass on Linux, and the tree-wide profile
check passes.
</details>
---
## Starter profiles
Three vendors ship belt profiles. Belt mode needs the printer settings
at **Advanced** or above to show its group.
<img alt="advanced mode"
src="https://github.com/user-attachments/assets/7a519ce5-b3b5-400c-a914-4f208bb577b0"
/>
| Printer | Vendor bundle | Nozzles | Processes | Filaments |
|---|---|---|---|---|
| **Generic Belt Printer** (`MyBeltPrinter`) | Custom | 0.2, 0.4, 0.6,
0.8 | 0.20mm Standard, 0.12mm Fine | library |
| **BabyBelt Pro** (Printcepts) | Printcepts | 0.4 | 0.20mm Standard |
Generic PLA, Generic PETG, eSUN PLA |
| **IdeaFormer IR3 V2** | IdeaFormer | 0.4 | 0.20mm Standard | Generic
PLA, Generic PETG, eSUN PLA |
To set up a printer that isn't listed: pick **Generic Belt Printer**,
set the belt width and length, save the profile, then copy in your
machine's start/end G-code and limits and tune from there.
<img alt="generic belt printer"
src="https://github.com/user-attachments/assets/90134d55-d6fe-4dba-8695-5ea44e78ec2b"
/>
<details>
<summary>BabyBelt Pro</summary>
<img width="2467" height="1392" alt="BabyBelt Pro"
src="https://github.com/user-attachments/assets/2b448cee-339e-43c9-964b-1ee9044044c7"
/>
</details>
---
## Contributions
### @HarrierPigeon
***Majority of design & implementation***
I did most of the work here by myself with AI tools (primarily Claude,
some Codex.)
PRs #12733, #12998, #14385, #15155, #15361, #15156, #15526, #16127
### The @Unlayered3D Team
***Rotation-Mode Pipeline***
The initial version of this sheared the model in the pre-slice pipeline.
Talking with them convinced me to switch to the current
rotate->slice->unrotate-> remap & shear method, which had significant
immediate improvements. Working with them has been a blast.
### @tommasobbianchi
***IdeaFormer IR3V2 Profile, eSUN PLA Tuning, G-Code Render / Preview***
TommyB came in at the perfect time to help keep me motivated and
contributed several things I hadn't had the werewithal to implemement
yet. Without their contributions and encouragement, we wouldn't be here
yet.
### The BabyBelt Community
**BabyBelt Pro** — @rexit1982 for the profile, and @RobMink of
Printcepts for the printer and a lot of patient testing.
**Field reports** — Many members of the BabyBelt community helped,
testing on their machines, providing G-Code and examples of issues, and
encouraging me to keep working on it. Among them:
- @RobMink - creator of the BabyBelt
- @rexit1982 - initial BabyBelt Pro profile, bug hunter
- @shubhracc - found a *lot* of technical bugs
- @NeoDLC - bug hunter
and BabyBelt Discord members who found bugs & gave feedback in no
particular order:
- @horatio42 - also provided a build machine while mine was down
- @HotCubCar - requested belt printer brim support
- Swap_File
- @Nyctelios
- Sup
- @matschi140
- @Rise-Run
- @shooby-dooby
### OrcaSlicer Maintainers
**Generic belt printer** — @SoftFever
**Keeping on top of upstream** @RF47 & @HanifKoh
**Review and fixes** — @HanifKoh (#15685 and the review on this PR).
Among them: plates after the first printed off the bed on belts; painted
supports and seams ignored the belt transform; support generation failed
at a 90° tilt; the object table crashed on the Support column on every
printer because tilt keys were in the per-object tables; apron brims
printed in the wrong filament; `build_plate_tilt` went stale outside the
GUI; the CLI reserved a wipe tower on belts; every G-code file was
treated as belt G-code because the config block carries the angle; tests
didn't compile on Clang/MSVC; plus a long list of smaller clean-ups and
the review questions that led to the clearance check, the header length
fix, the retired floor modes and the removal of the diagnostic logging.
### Additional Thanks
A special thanks to LDO Motors, who provided equipment for validating
multicolor, and my wife, who not only put up with with this obsession,
and the addition of three belt printers to our home, but has encouraged
me to keep going ever since I started this project six months ago.
---
## Known Issues
- The purge prism's first tool is chosen by the shared `ToolOrdering`
logic; on some layouts the print opens on the wrong filament and makes
one extra change at the thin tip of the prism (the "cannot absorb the
full purge volume" warning at a low height).
- Colour grouping in arrange is a soft cost: when the belt is too short
for clean runs, colours overlap rather than spill onto another plate.
- Three OrcaFilamentLibrary filaments still carry `filament_z_hop` 0.4;
belt profiles override it to 0.
Also: slicing at an angle other than the machine's (decoupled frame
tilt) is supported but not something the starter profiles exercise.
Make the connected-net layout affordable on a dense patch
Laying a patch out as a connected net cost ~175 ms on a 42k-triangle patch,
against ~21 ms for the unwrap it works from, and the gizmo asks for it on every
preview, overlay and bake. Measured on a real project the grid behind it ran
~19 million triangle-pair tests per net, nearly all of them misses: a cell
holds every triangle whose box touches it, and a candidate really meets a
couple of them.
Keep a bounding box with each stored triangle and answer those misses with four
comparisons instead of a full intersection. The net drops to ~53 ms with
identical output - the seam metrics on the test project did not move by one.
Two further attempts were measured and dropped, and are recorded in the comment
so they are not tried again: a free-space pre-check per chart came out slower,
because a folded chart lands against the net by construction and the cells
under it are occupied anyway, and splitting the boxes into their own array for
locality lost more to growing two vectors per bucket than it gained.
Also pick a pair's fold line from the longest boundary they share rather than
whichever edge came first, and grow the net strongest-adjacency-first rather
than breadth-first by area. Only the fold a chart is reached by comes out
matching, so a chart claimed across a short boundary leaves the long one it
shared with its true neighbour torn.
texture_unwrap_dump reports an unwrap from a saved project - charts, their
topology, folded triangles, where the texture is discontinuous and how long
those seams are. All of the above was found with it, and it is what keeps a
claim about this code honest; reading the 3D view and guessing had produced
three wrong diagnoses in a row.
The comment said the lambda takes off "the instance part" of m_origin.
On a belt m_origin has been rotated by on_set_origin() by then, so
m_origin minus the plate offset is not the instance shift. Say what the
code does: undo what point_to_gcode() added and what the writer took
off. Comment only.
Reported by raistlin7447 in the review of #14394.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The by-layer collect_layers_to_print() dropped every merged layer group
in which nothing prints, and the per-object overload drops every belt
entry that prints nothing. The merged groups are built only from the
per-object entries, so after the second drop every group holds at least
one entry that prints and the first never removed anything. Remove it
and keep its explanation at the drop that does the work. No output
changes.
Reported by raistlin7447 in the review of #14394.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The leading-edge-only brim is the outer brim cut down to the part's first
contact with the belt. The cut was taken at the first layer with
geometry, but with an overhang on the leading side that layer is the
overhang's tip, which is sliced before the part reaches the belt and
does not touch it. The cut then lay ahead of the part: a 30 mm fin
left a sliver of brim well ahead of a 20 mm cube, and a 40 mm fin, or a
leading brim length of 0, left none at all.
The footprint loop already finds the layers that touch the belt (their
contact band is not empty); record the first of them and cut there.
The new test slices a cube with and without a 30 or 40 mm fin over its
leading end and checks that the leading-edge brim is the same. It fails
without the fix.
Reported by raistlin7447 in the review of #14394.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The last headers with a using or namespace alias at namespace scope:
- TCPConsole.hpp imported boost::asio::ip::tcp into Slic3r::Utils for
two member declarations. The alias is now a private member of the
class.
- WebSocketClient.hpp declared four namespace aliases and a tcp alias
at global scope, each used only by the header. The names are spelled
out.
- Repair.hpp aliased CGAL::Polygon_mesh_processing as PMP in
Slic3r::tex2color. The three functions that use it declare the alias
themselves.
- PreciseSeam.hpp, Thumbnails.hpp and MarchingSquares.hpp used a
using-declaration or directive for one or two spots each; those spots
are qualified. Thumbnails.hpp's "PNG"sv default argument becomes
"PNG", which converts to the std::string_view parameter the same way.
- tests/sla_print/sla_test_utils.hpp had "using namespace Slic3r;" and
tests/filament_group/fg_test_serialization.hpp "using json =
nlohmann::json;" at global scope. The headers qualify their own names;
the two SLA test sources get the directive themselves.
Also removed: twelve type aliases in headers that nothing references
(ConflictObjName, CircleSqf, CircleSqd, TRawBuffer, DistanceFunction,
SamePair, ExtruderNozzleInfos, Vec2dEvent, Vec2dsEvent, Vec3dEvent,
t_option, t_optgroups, Plater::fs_path) and a duplicate
fn_ft_job_msg_destroy alias in FileTransferUtils.hpp.
46 files include the same header twice at file scope, outside any #if,
66 times in all:
Model.cpp included Model.hpp twice, Utils.hpp <algorithm> and
<string_view> twice, seven GUI headers <wx/dataview.h> and
<wx/artprov.h> twice. The second include of each is removed.
GCodeSender.cpp and GCodeSender.hpp have been commented out of
libslic3r/CMakeLists.txt since 2022 and their only two includes are
commented out as well. Both files go, with the commented lines, and
the CMake entry for SLA/SupportTreeIGL.cpp, a file that no longer
exists.
The mixed-colour metadata options are parallel per-slot arrays in the project
config. A project saved before they were sized per slot stores a single value
for the gradient ones, and one saved before they existed stores none. The GUI
sizes all seven to the filament count when it opens a project; the CLI kept
the stored arrays and exported one-element defaults for absent ones, so a
project it exported carried one-element arrays where the GUI writes one entry
per filament. Slicing is unaffected, every reader treats a missing entry as
not mixed / no gradient, but the GUI-vs-CLI comparison reported the four
gradient keys on every mixed-filament project.
The resize helper moves from PresetBundle.cpp, where it was file-local, to
PrintConfig.cpp next to set_filament_dev_options(). It creates an option the
config lacks before sizing it, a no-op for the bundle's project config where
all seven always exist. The CLI calls it with its filament count once the
project and loaded filaments are merged, after the check that every mixed
slot has a filament of its own.
The leading-edge-only brim is the outer brim cut down to the part's
first contact with the belt, and the cut was taken at layers().front().
Since the slicing frame starts at the belt below the footprint (#16236)
that is an empty lead-in layer whose contact lies ahead of the part, so
the cut removed the whole region and the brim type produced no brim at
all. Take the cut at the first layer with geometry.
The all-brim-types test now includes leading_edge_only, and a new test
checks that the brim starts no later than the part and covers fewer
layers than the outer brim.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Guard the layer count and the per-object layer collection against an
object that is left without a layer to print on a belt (the counting
loop stepped before begin() and front() was taken of an empty vector).
Check the belt temperature tower's embossed model before the current
project is replaced, not after. Only invalidate the support step of
objects that own a belt brim when an object is added or removed. The
empty-layers test now counts an extrusion only where material is laid
down along a move. The BeltBrim.cpp SEQUENCING note says exactly which
layers are read, and the machine-frame scale is 1/|sin|.
Remove more code that nothing calls: the kinematics inverse
(to_logical, apply_axis_remap_inverse, to_build_volume and the state
kept for them), the world_coordinates(), is_active() and
belt_brim_areas_by_layer() accessors, the PrintConfig overload of
physical_tilt() and the DynamicPrintConfig overload of
compute_belt_height_and_floor(). Comments in GCode.hpp,
BeltSliceStrategy.hpp/.cpp and PrintObjectSlice.cpp that described the
retired pre-slice remap and plane-evaluator still did; the purge-tower
width tooltip named the wrong switch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BeltBrim.cpp still described the brim as running inside the parallel
support step; it runs sequentially after it (generate_belt_brim). The
GCodeWriter, calib.cpp and calib.hpp comments referred to an inheritance
layout and a dynamic_cast that no longer exist.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
belt_remapped_bbox() had no callers; belt_min_z() and
m_belt_global_xy_correction were written but never read;
LayerTools::has_belt_brim was set but never read; belt_surface_z was a
named zero.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The machine-frame transform is a shear of cot(tilt) and a scale of
1/sin(tilt), not tan and 1/cos; fix the tooltip and the matching comment
in BeltGCode.cpp. The gcode_remap_* labels and tooltips were passed
through L() as variables inside a lambda, which the string extraction
does not see; pass L("literal") at the call sites.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A belt brim is clipped against the other objects on the plate and is
built with its object's support step. When an object was added or
deleted only the print-level skirt/brim and export steps were
invalidated, so the remaining objects kept brims clipped against objects
that were no longer there, or overlapping ones that had arrived.
Invalidate posSupportMaterial on every object in that case on a belt
printer. Also reword the comments that still described the global Z
offset as a minimum across all objects.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The first-layer rule that prints the outer wall first when a brim is
attached to it, and the brim_type change rule that regenerates the
perimeters for it, only knew btOuterOnly. btLeadingEdgeOnly, the belt
brim at the part's first contact, is an outer brim too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
posSlice's invalidation list did not include posDetectOverhangsForLift.
A re-slice starts the layers over with empty overhang regions while the
step stayed done, so GCode::needs_retraction() had no overhangs to test
against until something else invalidated it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
collect_layers_to_print() drops the belt layers that print nothing (an
object's empty lead-in), but m_layer_count still counted every object
and support layer, so "total layers count", the total_layer_count
placeholder and the M73 progress disagreed with the layer changes in the
file. Count with the same predicate, shared through
belt_object_layer_prints_something(). The by-object overload of
collect_layers_to_print() now drops those layers as well, so both print
sequences write the same layer changes.
Dropping the empty entries per object has one more effect on multi-
filament belt prints: a layer no longer selects a filament that it then
prints nothing with. On belt_project.3mf (two filaments, belt purge
tower) the T commands go from 472 to 106 while the extruded length per
filament is unchanged; each of the removed tool changes was followed by
no extrusion.
The empty-layers test now runs for both print sequences and also checks
"total layers count" against the layer changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
In by-object printing the cooling buffer is reset for every object, but
m_belt_in_band, which tracks whether the extrusion is inside the band
along the belt where the part fan stays off, kept the previous object's
value. If the previous object ended inside the band the next one never
emitted its band start marker. Reset it with the cooling buffer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
process_layer()'s sub-layer pass (several filaments in one layer without
a purge tower) calls set_origin() per instance like the main instance
loop, but not on_set_origin(), which on a belt printer runs the origin
through the belt transform. Add the call so both passes place the
instance the same way.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The writer hands set_first_layer_point_test() a point with the plate
origin (its own XY offset) already removed, but the test subtracted the
whole of m_origin, which carries the plate origin as well as the
instance shift. On a plate other than the first the point was moved by
the plate origin a second time and the band test looked at the wrong
spot. Subtract only the part of m_origin that is not the writer's
offset.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dda58b07cd stripped debug instrumentation from TreeSupport3D.cpp with a
script, and that script also deleted the loop in organic_draw_branches()
that trims every branch slice against the collision volume, the bed and,
on a belt, the belt plane. This is the generator every printer uses, not
a belt code path, and it is the one place where raistlin7447's export
fixtures differed from main with belt printing off. Restore the loop as
it was on main, with the belt-floor clip.
The new test prints a cube carrying a 60 mm plate with organic supports
on a flat-bed printer and checks on every support layer that no support
extrusion comes within 0.2 mm of the part's slice. It guards that
invariant; on this fixture the loop's own effect is a sub-millimetre
reshaping of one branch (verified by slicing the fixture with and
without the loop), below the asserted gap, so the test does not by
itself fail without the loop.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Classic tree supports (tree hybrid / slim / strong) on a belt slid down
the belt plane ahead of the part instead of landing on it.
`TreeSupportData` added the belt surface to every layer's outlines, so
the belt fed the collision and avoidance maps, and a node that descends
onto an obstacle is pushed out of it; on a tilted surface that walks the
branch down the belt. This takes the belt out of the outlines. The belt
is where a branch ends, and that is already handled: `drop_nodes()`
stops a node once its whole circle is in the belt
(`belt_node_landed()`), and `draw_circles()` clips every layer's circles
to the belt plane, so the branch tapers to a tip on it. Organic got the
same treatment in #16236 (the belt is no longer a support blocker
there).
One file, +7/−12. Non-belt printers are untouched: the removed block
only ran when the belt floor context was active.
## Before / after
Cube with a fin whose underside is parallel to the layers, 20 mm ahead
of the cube, tree hybrid, Left view:
| | support footprint along the belt | filament for support |
|---|---|---|
| before | belt Z 43–139 (sweeps 72 mm ahead of the part) | 2403 mm |
| after | belt Z 60–139, columns parallel to the up direction | 1606 mm
|
Organic on the same model: belt Z 74–139 (unchanged). Before/after
screenshots follow in a comment.
## Tests
- *Belt supports reach the belt under a leading overhang* passes for
normal, organic and tree_hybrid; all `[belt]` tests pass;
`fff_print_tests` 355 and `libslic3r_tests` 1116 pass on the branch.
- `scripts/clang_tidy_diff.py --base upstream/belt-printer`: no
findings.
- Fork CI (Build all) on this change: unit tests green on Linux x86_64,
Linux aarch64 and macOS arm64
(https://github.com/HarrierPigeon/OrcaSlicer/actions/runs/37601644023;
its Windows and slice-check failures are the ones #16262 fixes).
- Scripted GUI pass on belt-printer + this change: tree hybrid, organic
and normal supports at Y≈120 all reach the belt (lowest 0.17–0.19 mm);
with the part within its height of Y = 0 all three generators now behave
the same (support before the belt start, plate-boundary error shown),
where tree hybrid used to be the odd one out (clipped, hanging 9.5 mm
above the belt).
- Written with Claude Code; reviewed and run by me.
TreeSupportData added the belt surface to every layer's outlines, so the
belt fed the classic tree's collision and avoidance maps. A node that
descends onto an obstacle is pushed out of it, and on a belt that walked
the branch down the tilted surface, ahead of the part, before it could
end: tree hybrid/slim/strong supports swept far along the belt where
organic supports dropped straight down. Take the belt out of the
outlines. The belt is where a branch ends, and that is already handled:
drop_nodes() stops a node once its whole circle is in the belt
(belt_node_landed()) and draw_circles() clips every layer's circles to
the belt plane, so the branch tapers to a tip on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since the slicing frame of a belt object starts at the belt below its
leading end, its first layers are empty. On a single part they carry
the brim bands; with several parts along the belt the later parts'
empty layers fall between the earlier parts' printing layers and were
written to the G-code as layer changes with no moves at all. The
preview numbers its layers (libvgcode::Layers) from the vertices it is
given and expects consecutive ids, so at the first such gap it stopped
creating layers and folded everything after it into the last one: the
top slider layer held nearly the whole print, the slider jumped every
other layer through the single-colour stretch before a second part on
another filament, and with the belt purge tower the whole print greyed
out while dragging.
Drop the belt layers that print nothing (no object, support or brim
content) in GCode::collect_layers_to_print, and renumber the layers
consecutively over the moves that exist when converting a result for
libvgcode, so a file with empty layers from any source still previews
correctly. The layer slider labels a belt layer with its print Z (the
slicer's layer Z, which increases along the belt) instead of libvgcode's
toolpath height, which on a tilted layer is wherever its last extrusion
ended; the slider assumes that list increases and showed "0 / max" on
alternate layers. The processor reads that print Z from the ";Z:" tag
non-BBL printers write (it only knew "; Z_HEIGHT:"), on belt printers
only, so nothing changes elsewhere. Regression test: two cubes 60 mm apart along the
belt produce no layer without an extrusion and the header's layer count
matches.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The device drying options hold several values per filament, as many as
the filament preset gives, and a project stores them as the filaments'
values one after another. The CLI filament merge wrote them like an
option with one value per filament, putting each preset's first value at
the filament's own index, so a project with three filaments whose preset
gives "1", "0" was exported with 1;1;1;0;1;0 where the GUI writes
1;0;1;0;1;0.
The merge now leaves these options out of the per-filament pass and
rebuilds them afterwards from every filament's values in slot order.
Without a fixed number of values per filament one slot cannot be
replaced in place, so the stored values are kept when any slot has no
config to rebuild from.