Brings `belt-printer` up to date with `main` (4b4a261787) so that #14394
merges cleanly again, and adds the follow-up fixes the merge needs. This
PR targets `belt-printer`, not `main`.
## Commits
1. **GCode: hold the writer by value again.**
- Belt printing had turned `GCode::m_writer` into a `unique_ptr`, so
that `BeltGCode` could swap in a new writer carrying the belt
kinematics.
- Nothing subclasses `GCodeWriter`, and `set_kinematics()` can install
the belt mapping on the existing writer. This commit removes the swap.
- About 190 `m_writer->` edits revert, which takes `GCode.cpp` from 29
conflict hunks with main down to 3.
- The G-code is identical to the current `belt-printer` head on two
BabyBelt projects (see Verification).
2. **Merge upstream/main.** The resolutions are listed in the merge
commit. The ones that needed a decision:
- `write_belt_header()` follows main's relocated header block (#15897,
#15915).
- First-layer acceleration keeps the per-path first-layer plane test,
now with main's cached nozzle index (#16028).
- The arc-to-polyline fallback moves into the out-param
`extrude_arc_to_xy`, which is the overload `GCode` now calls (#16108).
- The belt fields join `GCodeProcessorResult`'s forwarding assign.
- The Clipper2 renames (#15969).
- Belt printers still reserve no CLI wipe tower (#15837).
- The new sparse-layer tower options are hidden for belt printers
(#15841).
3. **Belt: register the raw G-code toggle as a Preview shortcut.**
- Main's assignable shortcuts (#15706) replaced the key switch that
carried **B**.
- The toggle is now `ToggleBeltRawGcode`, bound to B in the Preview,
where B was free. It is listed in the shortcuts dialog and can be
rebound, and the legend shows whichever key is bound.
4. **Precise Seam: slice modifiers in the belt slicing frame.**
- The new `slice_single_volume_regions()` (#16072) sliced modifiers with
`trafo_centered()`, so on belt prints the modifier regions landed in the
unrotated frame.
- It now uses `trafo_sliced()`, as the seam enforcers and support
volumes already do. On non-belt printers the two transforms are the
same.
- A regression test is included.
5. **Belt profiles: inherit what they repeat and pass main's profile
checks.**
- The BabyBelt Pro and IR3 V2 filaments name their single extruder
variant, as the library-based filaments of other vendors do, and drop
overrides that repeat the library value.
- The Custom belt base inherits `printer_extruder_id` from its parent.
- `normalize` drops the obsolete keys. `fix-variant` gives the machine
limits their silent-mode entry, which they previously read from the
single value.
- All three vendor versions are bumped.
- Printcepts and IdeaFormer keep their own machine and process bases,
because only filaments can inherit across vendor bundles.
6. **Profile validator: accept a belt printer's tool change without a
tower.**
- The slice sweep (185cfe4323) forces a prime tower and requires its `CP
TOOLCHANGE START` block.
- Belt printers have no wipe tower: they purge into a prism object.
Their filament change is the plain `T` command, which they emit
throughout the slice.
- On belt printers the validator now looks for that `T1` line instead.
## Verification
All on Linux.
- **`m_writer` equivalence.** The current `belt-printer` head
(b22384a559) and commit 1 were each built and used to CLI-slice two
BabyBelt benchy projects (389k and 804k lines of G-code). The output is
identical apart from the per-run object ids in the `printing object …
id:` comments.
- **Merged branch, belt-specific checks.**
- The belt header is written, no `;_BELT_BAND` markers leak, and the
belt axis never steps back.
- Against the Oct 3 belt + main merge, the only G-code differences are
fill ordering on a few layers and time estimates. Both come from main's
changes since then.
- Against the pre-merge output the differences are much larger. That is
expected: main's CLI now refreshes a project's settings from its system
presets (#15953, #16038), so for example `z_hop` follows the belt
profiles' 0.
- **Tests.**
- `fff_print_tests`: 345/345 test cases pass, including 30 `[belt]`
cases.
- `libslic3r_tests`: 1081 passed, 2 skipped.
- The new Precise Seam test fails 266 of its 284 assertions with the fix
reverted.
- **Profiles.**
- `scripts/orca_profile_tool.py check` passes for all 69 vendors.
- `OrcaSlicer_profile_validator -s -l 2`: all 1271 slices succeed.
Before commit 6, the six belt printers failed.
- A flattened before/after snapshot of every belt preset shows no value
a belt printer reads has changed.
- **GUI** (BabyBelt Pro, clean datadir):
- B switches the Preview between the designed and the raw machine-frame
G-code, and pressing it again restores the view exactly.
- The legend reads "Show raw G-code (belt only) [B]".
- The shortcuts dialog lists the toggle under Preview → Display as
rebindable.
- The sparse-layer tower options stay hidden in Advanced and Expert
modes.
Main has since gained one CI-only commit (f3d0b8a553), which merges
cleanly on top.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01AJzy1xeQV3FePh5HfahDyn
The validator slices every printer with two filaments and the prime tower
forced on, then requires the tower's CP TOOLCHANGE START block as proof
that change_filament_gcode ran. A belt printer has no wipe tower: it purges
into a prism object on the belt, so Print::has_wipe_tower() is false and
the change is the plain T command set_extruder() emits. All six belt
printers failed the sweep on that alone, although each one changes
filament throughout the slice. Look for the T1 line on belt printers.
main's profile checks reject the belt bundles: their filaments override
variant keys with one value under the library's six-variant presets, the
copied vendor commons carry keys the slicer no longer reads, the IR3 V2
and BabyBelt Pro machine limits miss the silent-mode entry, and the Custom
belt base pins printer_extruder_id to one entry under a three-variant list.
- The BabyBelt Pro and IR3 V2 filaments name the one extruder variant
their printers have, as the other vendors' filaments built on the
library do, and drop every override that only repeats the library value
(diameter, density, temperature range, most of the fan settings...).
The eSUN filaments inherit the narrowed list.
- The Custom belt base inherits printer_extruder_id from its base.
- normalize drops silent_mode, adaptive_layer_height and
tree_support_with_infill; fix-variant gives the machine limits their
silent-mode entry, which they read from the single value before.
- The IR3 V2 drops two limits equal to its base in both modes.
- Custom's index is regenerated and all three vendor versions bumped.
Flattening every belt preset before and after, nothing a belt printer
reads changes: printer_extruder_id is one id per variant, all 1.
Printcepts and IdeaFormer keep their own machine and process bases: only
filaments can inherit across vendor bundles (from OrcaFilamentLibrary), so
they cannot build on the Custom belt printer.
slice_single_volume_regions() sliced Precise Seam modifiers with
trafo_centered(), but a belt printer slices its layers with
trafo_sliced(): the belt rotation, any pre-slice remap and the lift off
the plate on top. On a belt print the modifier regions landed in the
unrotated frame, away from the walls they were meant to place the seam
on. Slice them with trafo_sliced(), as the support volumes and the seam
enforcers already are. It equals trafo_centered() off a belt printer.
Main's assignable shortcuts replaced the canvas key switch that carried
the belt "show designed / show raw G-code" toggle on B. Register it as
ToggleBeltRawGcode, bound to B in the Preview (B is only taken on the
Plater, by the mesh boolean gizmo), so it can be rebound and is listed in
the shortcuts dialog. The legend checkbox shows whatever key is bound.
Brings belt-printer up to main 4b4a261787. Resolutions:
- G-code header (#15897, #15915): main moved the header, config and
thumbnail block later in _do_export; write_belt_header() moves with it,
still after the thumbnails and outside the BTT_TFT gate.
- _extrude: first-layer acceleration keeps the per-path first-layer plane
test with main's cached nozzle index (#16028); main's set_speed out-param
form (#16108) everywhere else.
- GCodeWriter (#16108): the arc-to-polyline fallback for machine mappings
that cannot express G2/G3 now runs in the out-param extrude_arc_to_xy,
which is the overload GCode calls, and appends to the caller's string.
- GCodeProcessorResult: the belt fields join main's forwarding assign.
- Clipper2 (#15969): belt arrange helpers take Slic3r::Point; the tree
support join types lose their ClipperLib qualifier.
- CLI arrange (#15837): belt printers still reserve no wipe tower.
- Wipe tower options (#15841): the two new sparse-layer toggles are hidden
for belt printers like the rest of the tower options.
- Keyboard shortcuts (#15706): main's registry replaces the old key switch;
the belt view toggle is re-registered in the next commit.
- Print::process: the belt purge-plan undo runs before main's SliceStarted
event.
- scripts/filament_id_snapshot.json: deleted on main (a77209af8f).
- Includes and appended tests: union of both sides.
Belt printing turned GCode::m_writer into a unique_ptr so BeltGCode could
swap in a freshly built writer carrying the belt kinematics. Nothing
subclasses GCodeWriter: the machine mapping lives in its MachineKinematics,
which set_kinematics() installs on an existing writer. A GCode is built for
every export and the only state on the writer when init_belt_writer() runs
is the plate offset, which the swap had to copy across by hand.
Install the belt kinematics on the writer in place, drop the copied offset,
and drop the virtual markers on GCodeWriter that the old subclass needed.
Every m_writer-> in GCode.cpp goes back to m_writer., which is most of the
belt diff in that file and most of its conflicts with main.
The pressure-advance pattern keeps its shared_ptr writer: the unique_ptr
kinematics make GCodeWriter move-only and that class must stay copyable.
# 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.
BeltGCode is only created for belt printers, so its hooks no longer re-check belt_printer, and the BBL-machine flag is set once on whichever writer survives init_belt_writer instead of on one about to be discarded.
The bed gravity arrow, volume rendering and the painter/support gizmos each rebuilt
the tilt up-vector from build_plate_tilt_x/y; use one helper that also tolerates
presets without the keys.
Print::has_wipe_tower() is always false for belt printers, but CLI arrange, plate checks and the pre-slice tower clamp still reserved a phantom tower footprint and wrote a clamped wipe_tower_x/y into the config.
Extension layers were all created with id 0, so every one of them could be taken for the first layer by id-only checks (ooze-prevention standby temperature, cached layer ids). Renumber the support layers after inserting them.
gcode_back_transform, first_layer_plane* and belt_printer_infinite_y fell through to invalidate_all_steps(), which re-ran tool ordering, skirt/brim and G-code export on toggles that only affect G-code export.
Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange.
The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged.
toggle_options() now runs on every value change and mode switch; rebuild the
brim_type choices only when the leading-edge entry has to be added or removed.
Every printer's config block lists belt_slice_rotation_angle (default 45), so the processor marked all G-code as belt G-code: imported flat G-code got the belt view on a belt printer, and the belt-only Z handling in the processor ran for non-belt prints whose config block precedes the body. Take the angle only from outside the config block, where only the belt header writes it.
A 90 degree tilt has no finite gravity drift per layer, so the option range
now stops at 89 degrees, matching the cap applied by the support generators.
The three support generators each computed lh * tan(tilt), which overflows
coord_t at 90 degrees and flips sign beyond it (belt sync can write up to
180). One helper now returns the tilt slope with the tilt capped at 89 degrees.
Painted support/seam facets, support volumes, seam occlusion, MMU and fuzzy skin painting (top/bottom
and side facets) and the adaptive infill octree used trafo_centered(), or trafo() with a centre-offset
shift, while the layers were sliced with the belt rotation, remap and Z lift; they now share
PrintObject::trafo_sliced().
The belt writer replaced the plate-offset-carrying writer mid-export, so belt G-code for any plate but the first kept the plate origin and long-travel clipping used the wrong frame. GCode now remembers the offset and hands it to the new writer, and the writer's first-layer probes use the plate-local point it emits.
build_plate_tilt_x/y are printer-preset keys; listing them in the per-object
frequent-settings and object-table bundles stored ignored values in object
configs and crashed the object table on the process config lookup.
Merge origin/main (00429da739) into belt-printer.
Conflicts resolved:
- src/CMakeLists.txt: keep both wxInspector workarounds.
- GCodeProcessor.cpp: keep the belt compare_pos / z_for_height lines.
- PrintObjectSlice.cpp: the belt bbox-Z guard also covers main's
printable_region_ids bookkeeping.
- TreeSupport.cpp: the belt-floor check runs before main's PendingNode
queueing.
- Tab.hpp: keep the belt fields, drop the removed upload description
fields.
- tests/libslic3r/CMakeLists.txt: keep both test files.
Also included:
- eSUN PLA belt presets declare their own filament_id (OFkrxQC4) and
scripts/filament_id_snapshot.json is regenerated, as main's filament_id
check requires.
- Custom.json version bumped to 02.04.00.05 so the belt entries reach
existing installs.
- Fix the ambiguous WithinRel call in the belt apron width test, which
otherwise breaks the fff_print build.
A mixed filament slot is virtual: ToolOrdering::resolve_mixed_filaments()
replaces it with its physical components before any G-code is emitted, so
the toolchanges the prism has to absorb are between those components.
ensure_belt_purge_tower() counted the slot as a filament of its own,
provisioning one island per mixed slot that no swap can ever reach -- the
"extra purge tower" on MCTEST5, where filament 5 is a 50/50 blend of 2
and 4 and the G-code reports 0.00 g of it used.
Expand the assigned set with the same expand_mixed_filaments() the
backend uses, so the GUI sizes the prism against the filament set the
slicer actually produces. No-op when nothing is mixed. Test covers the
MCTEST5 shape, a mixed slot whose components are otherwise unused, and
the no-mixing case.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ
Two independent leaks of filament on the belt purge prism, plus the
replan safety net the second one needs.
1. The early-truncation scan bounded itself with the prism's own
toolchanges. ToolOrdering covers the whole print and the prism is a
printed object in it, so the "last toolchange" the scan found was on
the prism's own top layers -- it runs past every model object by
design -- and the truncation cancelled nothing. Bound the scan at the
tallest non-prism object (support layers included; on a belt they can
top the object). On MCTEST5 that was 197 toolchanges over 39.4 mm of
tower that no swap ever needed.
2. On a layer with no toolchange, the prism's entire fill printed as
solid infill in its own filament. Drop the fills no toolchange
claimed, right after the purge marking and before
ensure_perimeters_infills_order() force-overrides whatever is left.
Perimeters stay so the bar keeps a continuous wall. An earlier version
of this deleted the entities and had to be reverted: psWipeTower can
rerun without regenerating infill, and a later tool ordering may claim
what this one did not. The entities are now stashed with their layer,
region and index and put back exactly, the same reversibility contract
layer truncation already had.
3. Both stashes go stale if an object step reruns: make_fills() clears
and regenerates fills over m_layers only, so a stale stash would put
old fills back next to new ones, and truncated layers would keep old
perimeters/fills. Undo the plan's edits at the top of Print::process()
whenever psWipeTower is not done. Every object-step invalidation also
invalidates psWipeTower, so that condition is exactly "some object
step may rerun"; when it is done nothing regenerates and the edits
must stay. This also covers a prism left behind after belt mode is
turned off, which previously stayed truncated forever.
WipingExtrusions::is_entity_overridden() becomes public so the prism can
tell claimed fills from unclaimed ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ
Fixes the report in #12998 (comment 5465250754): first-layer speed and the
slow_down_layers ramp were ignored on a belt printer. The report reads as a
per-object problem, but neither applied to *any* object -- the reporter's first
part slowed down because slow_down_for_layer_cooling was on, which is
CoolingBuffer's time-per-layer mechanism, not initial_layer_speed.
FirstLayerPlane decides first-layer-ness by perpendicular distance to a plane it
derives by composing gcode_remap_* with compute_machine_z_affine(). The plane is
therefore a function of how G-code is *addressed*, not of where the belt is:
change the output axis convention and the plane moves. On MCBELT-TYPE2 the
first layer measured 86.2 mm from the plane and got effective index 431, far
past any slow_down_layers ramp.
on_first_layer(point) and effective_layer_index_for_point() now measure height
above the belt surface, using the belt description already carried in
SlicingParameters -- belt_floor_shear_factor / belt_floor_from_axis /
belt_floor_z_shift -- the same description the support generator uses. That is a
property of how the object was sliced, so no remap or back-transform can perturb
it.
Deliberately not via BeltFloorContext: its init() folds in
belt_support_floor_offset, a support-generator diagnostic, and letting that
option steer the model's first-layer speed band would be a surprising coupling
(a negative value would switch the slowdown off outright).
Preserving the existing first-layer-plane settings:
* first_layer_plane XY/YZ/XZ keeps the FirstLayerPlane evaluator, as those are
explicit opt-outs.
* A non-zero first_layer_plane_offset also keeps it. The offset is a machine-Z
shift that FirstLayerPlane converts into a perpendicular distance in the
slicing frame; this evaluator measures along slicing Z, so there is no
faithful translation. Deferring to the evaluator that implements the setting
beats silently ignoring it.
* The two thresholds stay separate, exactly as FirstLayerPlane keeps them:
the first-layer boolean tests initial_layer_print_height, while the
effective layer index counts bands of first_layer_plane_thickness.
Brim and coincident apron bands are emitted before m_layer is switched to their
object -- for an apron band there is no Layer at all -- so both paths publish the
belt-floor owner explicitly. Without that a brim's classification would borrow
whichever object was visited previously, making it depend on plate order.
Note that first-layer-ness drives more than speed: extrusion acceleration, jerk,
the first-layer flow ratio and eligibility for overhang speed/fan analysis all
read it, so all of them are corrected on belt printers by this change.
Classification still samples only each path's first point, as it did before.
Non-belt is unaffected by construction: belt_height_above_floor() returns false
when the belt floor is inactive and both call sites fall back to the previous
path. FirstLayerPlane stays in place for its other modes and for CoolingBuffer,
whose machine-coordinate probe is a separate outstanding bug.
Measured, MCTEST4 on MCBELT-TYPE2 (initial_layer_speed=5, slow_down_layers=40):
15 distinct feedrates with no gradient and F300 absent, becomes 70 including the
full ramp 300(5) 382(6) 465(8) 630(10) 795(13) ... Two bare cubes on a belt:
0 slow extrusions becomes 2378 across Z 32.36..95.18. The same two cubes on a
Cartesian printer keep their slow extrusions confined to Z 0.20..2.00.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
1. Belt tree support could not slice at all.
layer_initialize() hardcodes layer 0's bottom_z to 0, encoding "below layer 0 is
the build plate at z = 0". True for a flat bed; false for a belt, whose virtual
support layers legitimately extend below zero. The bottom-most belt layer
therefore got height = print_z - 0 = -9.8, which reached Flow::with_height() and
threw FlowErrorNegativeFlow.
A 3DBenchy, a mushroom, an L-bracket and an extruded L all failed identically
with negative flow / return -100. Only a bare cube sliced, because its support
never reached that far down.
The bottom is now taken from the previous layer's z, and only a layer 0 whose
print_z is itself negative gets a synthesised bottom below it. Every
non-negative print_z -- every non-belt configuration -- keeps exactly the
previous 0, so this is behaviour-preserving off a belt by construction. An
earlier form used min(0., layer_z(0) - layer_height), which regressed flat beds
whenever the initial layer was thinner than the layer height.
3DBenchy on a 45-degree belt with organic tree support: fails to slice ->
247 support blocks / 168,596 extrusions.
2. Support generated against the belt, and against belt-tilted walls.
A plain 20mm cube on a 45-degree belt generated 86 support blocks and 46,307
support extrusions. Three causes, all gated on the belt floor being active:
a. The build-plate tilt compensation shifted the lower layer the wrong way.
tan(build_plate_tilt_*) carries a magnitude but no direction, and the sign
chosen moved the lower layer away from the newly appearing material rather
than under it, doubling the mismatch. The shift now comes from
belt_floor_shear_factor / belt_floor_from_axis, which carry sign and axis
exactly. Non-belt tilted beds keep the previous behaviour.
b. Material resting on the belt was treated as unsupported. The belt surface
is now unioned into the effective lower layer, sampled at the bottom of the
layer -- a layer meets the belt across its thickness and print_z is the
top. The half-plane is clipped to the layer's bounding box first: unioning
a +/-1000mm half-plane raw with 20mm-scale geometry put a huge dynamic
range through Clipper and left intermittent artefacts every few layers.
c. The object's first slice can be empty on a belt (the bottom vertex is a
sub-extrudable sliver), leaving the layer above with an empty predecessor
even though it rests on the belt. (b) already covers that per island. What
did need fixing is sharp-tail detection, which tests each island against
the raw lower slices; with an empty predecessor that test is trivially true
and every belt-contact island read as a sharp tail. It now tests against
the same effective lower layer.
An earlier form instead skipped the whole layer when the point of
get_extents(curr_polys) -- the bounding box of the union of every island --
nearest the belt was in contact. That was wrong in a way worth recording:
one island resting on the belt could suppress overhang and sharp-tail
detection for a separate island floating well above it. Every decision here
is per-island.
Cube on belt: 46,307 -> 0 support extrusions. Same cube non-belt: 0 before and
after. Benchy on belt still 247 blocks / 168,596 extrusions and a mushroom
111 / 82,157, so false positives are removed without suppressing true ones.
Non-belt is unchanged by measurement, not only by the belt_ovh_active gate:
the same mushroom sliced on a Cartesian printer before and after gives 65,866
support extrusions and 68,717 total extrusions both times, the two G-code
files differing in exactly one line -- the object's plate position.
3. m_anti_overhang was filled and read in different index spaces.
It is consumed in the same index space as m_layer_outlines, where object layer i
lives at num_raft_layers + i, but was filled in object-layer space. Every entry
landed num_raft_layers too low (50 for a 20mm cube at bed Y=50) and the topmost
object layers got none. The belt injection also ran before m_raft_layers was
extended, so it could not have known the offset.
The array is now shifted as a whole and the injection moved after the raft
extension. This also repairs user support blockers under a raft, which is not
belt-specific: it changes behaviour for any ordinary raft, not just the belt's
virtual one, and should be reviewed as a general fix. Measured effect on the
cube was small on its own (46,307 -> 46,334 before the other fixes) because
m_anti_overhang only feeds calculate_placable; kept as a correctness fix on its
own merits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
Both change emitted G-code, which is why they were kept out of the extraction
commit. Both are wrong only where the machine mapping is non-identity, which is
the definition of each bug.
1. Suppress lifts commanded through an unknown position.
_travel_to_z() emits full XYZ whenever the mapping must emit every axis, because
the mapping can make machine Z depend on logical X/Y, and it builds that point
from m_pos. At print start, and after any custom G-code that invalidates
position, m_pos.xy is the uninitialised origin; mapping (0, 0, z) through a
non-identity remap produces a real but wrong machine point -- for a reverse
mapping, build_vol_max, i.e. the far corner of the bed. The subsequent full-XYZ
move corrects the position, but the lift has already commanded a rapid across
the whole bed at travel speed.
Belt kinematics already guarded this; the Cartesian path did not. The guard is
now applied at all three lift sites through must_skip_lift_now(), not just the
one the extraction covered: travel_to_xyz()'s pending-lift branch,
lazy_lift(spiral_vase=true), and eager_lift(). The latter two also needed the
state fix -- both recorded m_lifted = target_lift regardless, so suppressing
only the emission would leave a later unlift() descending from a height that was
never commanded.
2. Never emit a G2/G3 arc a mapping cannot represent.
extrude_arc_to_xy() emitted G2/G3 with logical X/Y and I/J and never consulted
the mapping. There is no general fix by transforming the arc: a permutation
moves it out of the XY plane that I/J describes, a negation reverses handedness,
and the belt shear maps a circle to an ellipse that G2/G3 cannot express at all.
So supports_arc_moves() gates generation through the existing
GCode::should_disable_arc_fitting() hook, and BeltGCode's special-case override
is deleted -- belt now gets the same behaviour from the general rule instead of
its own exception.
supports_arc_moves() is m_remap_x == 0 && m_remap_y == 1, not !has_axis_remap():
an arc emits only X/Y/I/J, so a mapping that merely negates or reverses Z leaves
every emitted word untouched and keeps its arcs.
The fallback for an unrepresentable arc tessellates it into linear segments at a
0.005mm chord tolerance rather than substituting a single chord, and splits dE
proportionally across the segments. The capability check is hoisted above every
extrusion mutation: an earlier form ran it after filament()->extrude(dE) and so
extruded 2*dE on the fallback path.
Known limits of that fallback, since it is worth stating rather than discovering:
emitted relative E is conserved only to per-segment rounding (a radius-5
semicircle with dE=1.5 emits 1.50012 across 36 segments); the 0.005mm bound is a
logical-frame bound, about 0.00855mm in machine space under a 45-degree belt
shear; unequal endpoint radii and non-finite inputs are unchecked. Ordinary
export takes the original polyline when the mapping rejects arcs, so this path
is a fallback rather than the normal route.
Known gap, not claimed fixed: classic wipe towers have their own
enable_arc_fitting and their own G2/G3 emitter in GCode/WipeTower.cpp, which
should_disable_arc_fitting() does not govern. Belt printers are barred from
classic wipe towers; a remapped Cartesian printer is not.
Tests in tests/fff_print/test_gcodewriter.cpp: reverse-X remap with unknown and
with known position plus an identity control; eager_lift emitting nothing and
recording nothing; the arc-capability matrix including the Z-only cases; and the
tessellated fallback. E accounting is asserted through used_filament() rather
than E(), which resets per line in relative-E mode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
BeltGCodeWriter subclassed GCodeWriter and overrode seven methods, five of them
by copying the base body and changing the transform. The base writer already
carried an axis remap and already branched at each of its seven
coordinate-emission decisions; the subclass did the same branching with a
different transform, and the two copies had begun to drift.
Replace the inheritance with a strategy object owned by GCodeWriter:
CartesianKinematics to_machine = the existing apply_axis_remap; today's base
behaviour, moved rather than changed.
BeltKinematics to_machine = MachineFrameTransform o axis_remap o
BeltBackTransform, plus a world_coordinates variant for
the PA calibration generators.
New: src/libslic3r/GCode/MachineKinematics.{hpp,cpp}, GCode/BeltKinematics.{hpp,cpp}
Deleted: src/libslic3r/BeltGCodeWriter.{hpp,cpp} (341 lines)
Points worth a reviewer's attention:
* The predicate is must_emit_all_axes(), not couples_axes(). The base returns
true for any non-identity remap, including pure permutations that do not
physically couple axes, so the question is "must every axis word be
emitted", not a statement about kinematics.
* Every per-site word-omission branch is preserved. The base deliberately
emits X/Y only, or Z only, or drops Z when its quantised value is unchanged.
The strategy changes which transform applies, never whether words are
omitted.
* set_kinematics() replays the configured remap and build volume onto a newly
installed strategy, because BeltGCode::init_belt_writer runs before
GCode.cpp calls set_axis_remap/set_build_volume_max.
* uses_pointwise_travel_speed() preserves a pre-existing divergence rather
than introducing one: the base travel_to_xyz emits the raw configured travel
speed in its final branch, ignoring the first-layer value computed at the
top, whereas the belt path used the first-layer-aware value throughout. Both
are kept. Unifying them changes feedrates and belongs in its own change.
* The [BELT-DEBUG] block is deleted; it rate-limited itself with a
function-local static thread_local in the hot emission path, and this is the
commit that would otherwise have moved it into shared code.
This commit is intended to preserve existing export output. That is reviewed by
construction -- each emission site keeps its own omission branch and each policy
divergence is preserved -- and is NOT verified against a G-code diff corpus.
Building that corpus is the outstanding work here.
Two API-equivalence exceptions, neither reachable by any caller today:
* Belt kinematics with no plane pointer installed, m_is_first_layer true,
initial and normal travel speeds differing, travel_to_xyz() reaching its
final branch: the old belt writer selected the initial-layer speed, the new
writer selects the normal travel speed. The pending-lift and XY-only
branches keep their previous selection.
* Belt kinematics installed without set_force_normal_lift(true) and a
non-normal lift requested: the old belt writer forced a normal lift, the new
writer can take the slope branch.
The PA-pattern generator reaches the writer through explicit travel_to_z() /
travel_to_xy(), not travel_to_xyz() or the lazy/eager lift paths, and normal
belt export installs both the plane and the forced-normal-lift policy, so
neither exception changes output produced today. They are recorded because a
future caller could reach them.
tests/fff_print/test_gcodewriter.cpp was also not compiling before this branch:
it called writer.to_machine_coords(), a method that existed only on
BeltGCodeWriter. It never surfaced because the build targets OrcaSlicer, not
all, and BUILD_TESTS defaults to OFF, so that translation unit was outside every
compile path. Fixed here; the existing 30-degree coordinate assertions are kept
verbatim as the best available regression net.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
# Description
During the UI/UX improvements about a month ago, I got the transforms
wrong, and slicing at anything other than a 45 degree angle was
affected.
Validated on a baby belt pro at 30 & 45 degrees.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
## Summary
Fixes for the `belt-printer` branch ahead of upstreaming, in two groups
(10 commits). Targets `belt-printer` (not `main`) since group 2 fixes
the not-yet-merged Belt Printer Brims feature.
Every fix keeps non-belt (and brim-disabled) output unchanged; belt-only
behavior is corrected. All changed translation units and the two test
files were type-checked (`-fsyntax-only`); a full build + `ctest` still
needs to run in an environment with current deps.
## Group 1 — pre-existing belt-printer regressions
- **[HIGH] BuildVolume belt state not reset when leaving belt mode** —
toggling belt off (or switching belt→normal with matching bed geometry)
left the `BuildVolume` with `m_is_belt_printer=true` and inflated Y
bounds, so out-of-bounds objects were treated as printable on a normal
printer.
- **[HIGH] `GCodeProcessorResult::reset()` didn't clear belt fields**
(`belt_tilt_angle`, `belt_z_origin`, `preslice_remap_*`) — a reused
result corrupted a normal print's start-gcode preview Z.
- **[LOW-MED] `TreeSupport::drop_nodes`** — restored the single critical
section around node invalidation (the two `valid=false` writes had been
moved outside the mutex on the shared tree-support path); removed an
unused local.
- **[LOW] Support overhang hot paths** — avoid unconditional lower-layer
polygon copies when there is no build-plate tilt (`SupportMaterial`,
`TreeSupport3D`); untilted output matches upstream exactly.
- **[LOW] Render loop** — hoisted the frame-invariant slope
`up_direction`/`normal_z` (and their per-volume config lookup) out of
the per-volume loop.
- **[LOW] FDM-support "select by angle"** — restored the exact upstream
threshold when the build plate is untilted (the generalized form
differed for non-uniformly-scaled objects); tilted-gravity form kept
only under tilt.
- **[LOW] Printer tab tilt sync** — only clears the belt-derived
`build_plate_tilt` on a genuine in-place belt→off toggle (tracked,
seeded on preset load), no longer wiping a manually-set tilt.
- **[LOW / opt-in] Axis-remap G-code emission** — always emit full XYZ
under an active `gcode_remap_*`, apply the remap on all base
`travel_to_xyz` destinations, fall back to a linear lift for spiral/arc
under remap, sync `set_axis_remap` each export; fixed belt first-layer
travel speed. Identity/default output unchanged.
## Group 2 — Belt Printer Brims (#15155) fixes
- **[CRITICAL] Dropped brim at first belt contact** — a coincident brim
band on an object layer with no extrusion pass (zero-extrusion leading
slice, or belt support below the Z=0 floor with no coinciding object
extrusion) was never emitted. Now each coincident band's brim filament
is registered in `ToolOrdering`, each band is emitted exactly once in
its brim-filament pass, and an end-of-layer orphan sweep emits any band
whose object layer produced no visit.
- **[Multi-extruder] Wrong tool / double emission** — apron and
coincident bands now print once, in the correct brim-filament pass,
brim-first (were previously emitted with the active tool and could
double-emit per filament plan). Single-extruder / single-object output
is byte-identical apart from the previously-dropped bands now printing.
- **Inner-only predicate** — `has_belt_brim()` no longer reports a brim
(and no longer rejects the prime tower / spiral vase) for `inner_only` +
`brim_width=0` + leading/extra > 0, which produces no inner geometry;
mirrored in `wants_brim`.
- **ToolOrdering raft-gap comment** — clarified why raft-gap synthesis
is suppressed for all belt printers (belt has no rafts;
sub-object-bottom layers are apron / belt-support-below-floor /
lead-in). No behavior change.
- **Tests** — deterministic coverage: brim present at first belt contact
(support on/off), brim-before-perimeters once (no drop/double), single-
and multi-extruder tool selection with no doubling, multi-object
per-filament ordering, inner-only+leading-only not rejecting prime
tower/spiral, and inner-ring / leading-edge-only geometry units.
## Testing
- `-fsyntax-only` passes for all 16 changed source TUs + 2 test TUs
against this branch.
- Please run the full build and `ctest -R 'SkirtBrim|BeltBrim'` before
merging.
## Known follow-up (out of scope)
`extrude_arc_to_xy` does not remap its I-J center, so arc-fitted
*extrusions* under standalone axis-remap would be geometrically wrong —
a separate fix if that combination is supported.
Opened as **draft**.
Deterministic tests for: coincident brim at first belt contact not dropped
(C), single- and multi-extruder brim tool selection with no doubling (B),
multi-object apron ordering, inner-only+leading-only not rejecting prime
tower/spiral (D), and inner/holed + leading-edge-only geometry.
- Emit coincident belt_brim_by_layer bands even when the leading object layer
has no InstanceVisit (zero-extrusion lead-in / no coinciding support), so the
brim at first belt contact is no longer dropped.
- Register each coincident band's brim filament in ToolOrdering and emit each
band exactly once, in its brim-filament pass; emit ordinary-layer aprons in
the brim pass before object extrusion (correct tool, brim-first) instead of
with whatever tool was active.
- has_belt_brim(): inner-only brims need brim_width>0 (leading/extra produce no
inner geometry), fixing spurious prime-tower/spiral rejection; mirror in
wants_brim. Single-extruder/single-object output is unchanged except
previously-dropped bands now print.
- BeltGCodeWriter::travel_to_xyz final branch used config.travel_speed
instead of the computed first-layer-aware travel_speed.
- extrude_to_xyz decided emit_xyz vs emit_xy from pre-remap Z; emit full
XYZ whenever an axis remap is active so remapped machine-Z is never
dropped.
- base travel_to_xyz now applies apply_axis_remap() on all emitted
destinations (standalone remap on non-belt printers was unremapped).
- spiral/arc travels fall back to normal linear lift under active remap
(endpoint-only remap can't preserve arc plane/I-J).
- set_axis_remap() is now synced unconditionally each export to avoid a
reused writer retaining a stale non-identity mapping.
update_fff() zeroed any build_plate_tilt matching the dormant belt-derived
tilt (default X/45) within 0.01, wiping a legitimate manual tilt on a
non-belt tilted-bed printer. Track the belt->non-belt transition and the
exact values belt-sync wrote, clearing only those on an in-place toggle;
reset tracking on preset load so preset switches never wipe tilt.
select_facets_by_angle replaced upstream's limit.dot(down) threshold with
cos(threshold), changing facet selection for non-uniformly-scaled/mirror
objects on ALL printers. Restore the exact upstream computation when no
build-plate tilt is active; keep the tilted-gravity form only under tilt.
Belt slope-shading changes recomputed up_direction (with a printer-preset
config lookup) and normal_z per volume; both are frame-invariant. Compute
once before the to_render loop and reuse the already-hoisted
support_normal_z. Uniforms are still set per volume; visuals unchanged.
SupportMaterial::detect_overhangs copied lower_layer_polygons per region
even without build-plate tilt; hoist the tilted copy out of the region
loop and use the original polygons directly when untilted. TreeSupport3D
flattened lslices_extrudable to Polygons unconditionally; restore the
upstream ExPolygons offset on the untilted path.
The 2-node merge moved the two valid=false writes outside the mutex that
upstream held together with the contact_nodes push_back; restore a single
critical section per branch (belt branch also guards to_buildplate).
Remove an unused top_interface_layers local in drop_nodes.
reset() cleared the sibling machine_frame_transform_active but not
belt_tilt_angle/belt_z_origin/preslice_remap_*; a reused result carried
stale belt metadata into a subsequent normal print, flipping the store_z
branch and corrupting start-gcode preview Z for non-belt prints.
Non-belt branch of set_bed_shape reset only the 3DBed renderer, not the
BuildVolume; Bed3D::set_shape early-returns on unchanged bed, so a
belt->normal switch or in-place belt toggle-off left the BuildVolume with
m_is_belt_printer=true and inflated Y bounds -> out-of-bounds objects
treated as printable on a normal printer.
# Description
This adds brim support to belt printers.
Added a new belt printer specific mode, Leading Edge Only and two new
belt-specific parameters, Leading Edge Brim Length, which increases the
number of brim lines on the side of the part printed first, and Extra
Brim Width, which increases the width of brims along the X axis. Because
belt printer first layers are effectively a single line, getting them to
stick properly can be a pain. This PR aims to help alleviate that, or at
least give more options for control.
<img width="1849" height="1043" alt="Screenshot from 2026-08-06
12-20-21"
src="https://github.com/user-attachments/assets/f963ed8e-53e7-48f8-a495-123cb9ae27f7"
/>
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
"Leading edge only" describes where a part meets a moving belt, so it has no
meaning on a fixed bed and should not clutter the Brim type dropdown there.
Filtered the same way support_style and wipe_tower_wall_type already are a few
lines above in TabPrint::toggle_options(): the field holds its own copy of the
option definition, and Choice maps the combobox selection straight onto that
copy's enum_values, so rewriting the values, the labels and the combobox items
together keeps the mapping correct.
The entry is kept when it is the current value, so opening a project that uses
it on a non-belt printer cannot leave the control displaying an option it does
not offer - which would silently rewrite the setting on the next edit.
Print::validate() already warns that it prints as an ordinary outer brim there.
Matches the scope of the existing precedents: the per-object override panel is
not filtered.
Six issues found by reviewing the previous commit against belt-printer, two of
them release-blocking.
Data race (high). Print::process() runs generate_support_material() for all
objects in a tbb::parallel_for, and make_belt_brim() runs at its tail, but
belt_brim_obstacles() read every OTHER object's support_layers() - which a
concurrent task may be inside clear_support_layers() deleting. That is a
use-after-free, and even when it survives, the obstacle set depends on which
object finishes first. Only this object's own supports are consulted now; they
are complete at that point. Foreign objects still contribute their slices,
which are finished and immutable before the support phase.
Apron bands dropped (high), two separate causes. An apron band prints below
its own object's first layer, but another object can already be printing at
that print_z, in which case process_layer() takes the ordinary path and never
emitted the band - the emission is now shared by both paths. Separately, a
band whose print_z matched a support layer of the SAME object was overwritten
in the print-wide merge, which keeps one record per object per z and could not
detect the collision because LayerToPrint::layer() is null for a band. The
per-object pairing loop is now a three-way merge over object, support and apron
streams, so each object contributes at most one record per z.
Multi-instance was far too strict (medium). It refused belt brim for every
multi-instance object, killing plain brim width and inner brim too, and only
warned when a leading length was set. Only movement ALONG the belt changes an
instance's belt-floor Z, so copies side by side ACROSS the belt share one set of
bands perfectly well; belt_brim_instances_compatible() now tests just that, and
the warning fires whenever the brim is actually suppressed.
Apron layer bookkeeping (medium). Apron layers count toward m_layer_count and
advance m_layer_index, but emitted no Z/height tags, left m_last_layer_z,
m_max_layer_z and m_last_height stale - so the first object layer computed its
height against a pre-apron Z - and skipped before_layer_change_gcode and
layer_change_gcode entirely. All of that now matches the ordinary path.
Obstacle cost (low). belt_brim_obstacles() ran a full-plate union per band.
A bounding-box pre-filter drops non-overlapping objects before materialising any
polygon, and the union is skipped for trivial inputs.
Deliberately unchanged: every apron band still reports cooling layer_id 0.
CoolingBuffer uses it for the initial_layer_fan_speed override and the
close_fan_the_first_x_layers gate, and every band lies on the belt plane itself,
so it is all first-layer material by the only definition that means anything on
a belt. Numbering the bands would ramp the fan up while still printing on the
belt. Now documented at the assignment rather than left implicit.
A belt printer slices in a rotated frame, so the belt surface is a tilted
plane rather than the Z=0 bed plane. Each slicing layer touches the belt
only along a narrow strip at its leading edge - about 0.2mm at 45 degrees -
so a part's first layer is really a first line, with almost no contact patch
to hold it down while the belt drags it forward. Brim was hard-disabled on
belt printers, leaving no remedy at all.
Generate the brim on the belt plane instead. The object's belt footprint is
the union over layers of each slice clipped to that layer's contact band; the
brim is offset from it in a "flattened" frame where the shear axis is
stretched by 1/cos(tilt), so ordinary Clipper offsets measure true on-belt
distance. It is emitted as cross-belt lines, one per layer band, anchored to
a fixed fraction of the band so every line shares a nozzle-to-belt clearance
and therefore comes out the same width; flow is matched to the resulting band
pitch, keeping the sheet uniform and gap-free.
Three new controls, all belt-only:
* Leading brim length - extends the brim ahead of the part along the belt,
on every downhill-facing edge of its contact area. This apron necessarily
prints BELOW the object's first layer, since layer 0 is the part's leading
contact, so it needs brim-only bands of its own.
* Extra brim width - widens the brim sideways across the belt only.
* Brim type "Leading edge only" - brim at the part's first belt contact and
nothing after it. Appended last in BrimType so no existing value shifts;
degrades to an outer brim off belt printers, with a warning.
The apron bands are lightweight records rather than a Layer subclass, so no
fabricated Layer::id() can leak into initial-layer temperature selection, the
spiral vase probe, cooling or gradual interpolation. They are generated in
posSupportMaterial because their print_z values must exist before ToolOrdering
is built at psWipeTower, and they are emitted from a short dedicated branch in
process_layer that runs before any layer pointer is dereferenced.
The footprint is closed before offsetting outwards: a belt contact patch is
often a broken-up strip, and the merged offset rings of two islands closer
than 2 x brim_width would otherwise fill the space between them - space that
lies under the part.
Also fixes a pre-existing bug where PrintObject::get_first_layer_bbox()
overwrote a valid bbox with an unassigned one on any belt printer with a brim
configured, because has_brim() was true while make_brim() returned early.
Belt brim is refused alongside the prime tower and spiral vase, and requires
one instance per PrintObject - translating an instance along the belt axis
changes its physical belt-floor Z. Untilted belt printers are unchanged: they
still get no brim, since the plate brim is emitted out of skirt_brim_groups(),
which _make_skirt() never builds for a belt printer.
The belt-printer branch is failing two profile gates. Both stem from the
three belt-only vendors (Custom's generic belt printer, IdeaFormer,
Printcepts) not existing upstream, so upstream maintenance passed them
by.
Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/
0.6/0.8 nozzle all fell back to "Default Setting". No process profile in
the Custom vendor listed any MyBeltPrinter in compatible_printers, and
Custom's fdm_belt_common pointed default_print_profile at "0.20mm
Standard @System", which does not exist in that vendor's index, so the
generic belt printer had no usable process at all. This gap dates to
when MyBeltPrinter was added (2026-04-07); it only started failing now
because the slice-check job is newer than that.
Adds two process profiles modelled on the sibling @MyKlipper ones:
- 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles
- 0.12mm Fine @MyBeltPrinter - 0.2/0.4 nozzles
The split is forced by hardware: the 0.2 nozzle preset caps
max_layer_height at 0.16, so a single 0.20mm profile cannot legally
cover
it. fdm_belt_common now defaults to the standard profile and the 0.2
nozzle preset overrides to the fine one.
setting_id: 14 files failed the rules introduced in #14432. That
migration renumbered 7425 files across 61 vendors but skipped these
three, leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter
squatting the "G*" id space reserved for Bambu (GMPC0BBP01, GMIF001,
GM_BELT_00x) and four instantiated filament/process presets carrying no
setting_id at all. Regenerated with
scripts/assign_vendor_setting_ids.py.
Also repoints the identical dangling "0.20mm Standard @System" in
Printcepts' and IdeaFormer's fdm_belt_common at their own real process
profiles. That is a no-op today because both concrete printers override
it, but it is the same landmine that took out MyBeltPrinter.
Vendor index versions bumped so check_installed_vendor_profiles() will
re-install the corrected profiles over an existing install.
Note: changing a shipped preset's setting_id can orphan user presets
that reference it as base_id. #14432 accepted that tradeoff for 61
vendors; this keeps these three consistent with the rest.
Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors
(was 14 files with errors), and OrcaSlicer_profile_validator -s slices
all 1015 printer presets successfully (was 4 failures).
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The belt-printer branch is failing two profile gates. Both stem from the
three belt-only vendors (Custom's generic belt printer, IdeaFormer,
Printcepts) not existing upstream, so upstream maintenance passed them by.
Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/
0.6/0.8 nozzle all fell back to "Default Setting". No process profile in
the Custom vendor listed any MyBeltPrinter in compatible_printers, and
Custom's fdm_belt_common pointed default_print_profile at
"0.20mm Standard @System", which does not exist in that vendor's index,
so the generic belt printer had no usable process at all. This gap dates
to when MyBeltPrinter was added (2026-04-07); it only started failing now
because the slice-check job is newer than that.
Adds two process profiles modelled on the sibling @MyKlipper ones:
- 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles
- 0.12mm Fine @MyBeltPrinter - 0.2/0.4 nozzles
The split is forced by hardware: the 0.2 nozzle preset caps
max_layer_height at 0.16, so a single 0.20mm profile cannot legally cover
it. fdm_belt_common now defaults to the standard profile and the 0.2
nozzle preset overrides to the fine one.
setting_id: 14 files failed the rules introduced in #14432. That
migration renumbered 7425 files across 61 vendors but skipped these three,
leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter squatting the
"G*" id space reserved for Bambu (GMPC0BBP01, GMIF001, GM_BELT_00x) and
four instantiated filament/process presets carrying no setting_id at all.
Regenerated with scripts/assign_vendor_setting_ids.py.
Also repoints the identical dangling "0.20mm Standard @System" in
Printcepts' and IdeaFormer's fdm_belt_common at their own real process
profiles. That is a no-op today because both concrete printers override
it, but it is the same landmine that took out MyBeltPrinter.
Vendor index versions bumped so check_installed_vendor_profiles() will
re-install the corrected profiles over an existing install.
Note: changing a shipped preset's setting_id can orphan user presets that
reference it as base_id. #14432 accepted that tradeoff for 61 vendors;
this keeps these three consistent with the rest.
Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors
(was 14 files with errors), and OrcaSlicer_profile_validator -s slices all
1015 printer presets successfully (was 4 failures).
BeltAffine activates the FirstLayerPlane evaluator unconditionally, so on a
non-belt printer on_first_layer(point) stopped agreeing with the legacy
slicing-layer-0 test. Every per-path first-layer call site in _extrude then
took the non-first-layer branch, and first-layer speeds were skipped: brim
came out at the volumetric fallback (24.6 mm/s) instead of initial_layer_speed
(10 mm/s). This is the shared speed path, so it affected all printers on this
branch, not just belt ones.
Auto resolves to BeltAffine only when belt_printer is set with a non-zero
slicing rotation, and to XY (evaluator inactive, legacy behaviour) otherwise --
exactly what the option's own description already promised.
Caught by "Brim uses first layer speed" (upstream #14616), which arrived with
the upstream merge; the bad default dates back to a9bae54f20 (#30). Verified
against a pristine upstream/main build, which passes the same test.
tests/fff_print: 100/100 test cases, 1085 assertions (was 99/100).
Both belt regression tests still pass, confirming Auto still resolves to
BeltAffine for belt printers.
Note: this changes a config default. Projects and profiles that stored
first_layer_plane explicitly are unaffected; those relying on the default will
now get correct first-layer speeds on non-belt printers, so their G-code
changes accordingly.
Processes a minimal belt start sequence through GCodeProcessor::process_buffer
and asserts the move preceding the first extrusion keeps its real Z, so it can
no longer back-transform to model Y~=0 and produce the phantom extrusion line.
Belt printers are non-Bambu, so the processor uses the compatible reserved
tags ("TYPE:"); the test sets s_IsBBLPrinter=false (saved/restored via an RAII
guard) to mirror the real printer. Proven to fail without the fix (the
prepare-stage move's Z is pinned to the first-layer height, 0 here) and pass
with it.
On a belt printer the sliced preview drew a stray extrusion-colored line
from Y~=0 to the model, rendered in the first extrusion role's color. It is
not a travel and does not occur on non-belt printers.
GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer
height during the start-G-code "prepare" stage. That is a harmless cosmetic
tidy-up on a normal printer, but on a belt printer the designed-view
back-transform couples machine Z into the rendered model Y (the belt tilt
mixes the height and belt-feed axes). Pinning Z back-transforms the last
prepare-stage move (the unretract before the first extrusion) to model
Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to
the first real toolpath.
Keep the real Z for belt printers (gated on belt_tilt_angle, parsed from the
G-code header before the body) so prepare-stage moves back-transform
correctly. Non-belt processing is byte-identical. The emitted G-code was
already correct; this is a preview-geometry fix.
Locks in the fix from the previous commit. A fresh BeltGCodeWriter has an
unestablished planar position (is_current_position_clear() == false) and its
m_pos.xy is the origin (0,0). With a pending NormalLift z-hop, travel_to_xyz
used to lift in place via _travel_to_z(), which in belt mode shears the origin
into a machine Y ~= the layer Z — a move far up the gantry.
The test configures an X-tilt 45 deg belt transform, defers a z-hop via
lazy_lift, travels to a near-belt first point (transformed gantry Y ~= 1mm),
and asserts no emitted move has Y anywhere near the layer Z. Verified to fail
without the fix (max emitted Y = 100.0 vs the destination's ~1.0) and pass with
it.
On a belt printer the first travel of the print emitted a bogus move to
the bed corner with the nozzle far up the gantry, e.g.
G1 X95 Y168.19 Z237.857 F12000
right after the first "; printing object" line. Y168 (≈ the layer Z)
is out of the gantry's range.
Root cause: the layer-change z-hop is deferred via lazy_lift and consumed
by the first BeltGCodeWriter::travel_to_xyz, whose NormalLift branch does a
separate lift-in-place via _travel_to_z(target.z()). On a normal printer
_travel_to_z emits a Z-only move, but in belt mode Z is coupled to Y/X, so
_travel_to_z re-emits the current m_pos through the belt shear. At print
start (and after custom gcode) m_pos.xy is still the uninitialised origin
(0,0), which the back-transform + axis-remap shear into machine
(X=bed_max, Y=layer_z) — the illegal move.
Guard the NormalLift branch on is_current_position_clear(), matching the
SlopeLift branch directly above it which already does so. When the position
isn't established there is nothing to lift over, and the xy_z_move that
follows travels straight to the destination with full XYZ, establishing the
correct position. Bookkeeping is unaffected: in this path m_lifted stays 0,
so no spurious restore move is produced.
Verified by re-slicing the repro project: the start-of-print move is now
G1 X44.946 Y.621 Z237.857 (straight to the first object point), no move
touches the bed-max X edge, and the max Y over the whole file is 62.8mm
(printable_height 100).
Upstream retyped travel_speed and travel_speed_z to ConfigOptionFloatsNullable
and initial_layer_travel_speed to ConfigOptionFloatsOrPercentsNullable, so the
scalar .value / get_abs_value() accessors no longer compile. BeltGCodeWriter.cpp
is belt-only and merged without conflict, so this only surfaced at build time.
Index them the way the base GCodeWriter does -- .get_at(m_cached_extruder_idx)
and get_abs_value_at(..., m_cached_extruder_idx) -- keeping belt's per-point
first_layer_for_point test rather than the base class's m_is_first_layer.
m_cached_extruder_idx moves from private to the existing protected block that
already exposes writer state to subclasses, so the belt writer resolves the
per-extruder index identically to the base writer instead of guessing one.
Brings the belt-printer work up to date with 591 upstream commits.
Conflict resolutions (12 files, 42 hunks):
- GCode.cpp: adopted upstream's per-filament/per-nozzle config refactor
(get_filament_config_index, NOZZLE_CONFIG), the extracted
generate_timelapse_gcode + farthest-point timelapse, and the
ConfigOptionFloatsNullable calibration options. Re-applied the belt
hooks on top: init_belt_writer / axis remap / FirstLayerPlane setup,
on_set_origin, the belt-corrected calib_z for the volumetric speed
tower, and path_on_first_layer (belt's per-path first-layer test) in
place of upstream's layer-index on_first_layer() in the acceleration,
jerk and overhang-detection paths. Swept upstream's new m_writer.
uses to m_writer-> since belt holds the writer by unique_ptr.
- interpolate_value_across_layers: kept upstream's banded stepping and
belt's object-Z-span ratio; dropped upstream's duplicate ratio decl.
- Plater.cpp: took upstream's guarded add_model(...) early-returns and
the VFA vfa_layer_height plumbing; kept the belt temp-tower path,
_calib_apply_belt_mode and belt_calib_flip_ringing_tower. Dropped the
VFA "cut upper" block, superseded upstream by model scaling.
- Brim.cpp: upstream's ObjectInstanceID-keyed brimAreaMap, keeping the
belt early-return.
- 3DScene.cpp: kept both the belt build-plate tilt up_direction and
upstream's per-extruder printable-height shading.
- GCodeViewer.cpp: kept upstream's dim-previous-layers setup and belt's
exemption from the same-result early return.
- TreeSupport.cpp: upstream's >= 0 roof-layer fix inside belt's
belt-floor branch.
- calib.cpp / GCode.hpp / GCodeWriter.{cpp,hpp} / Print.hpp: upstream's
additions adapted to belt's pointer-held writer and helpers.
- Custom.json: kept profile version 02.04.00.03 (belt) over upstream's
02.04.00.01; both bumped from 02.04.00.00.
Building this tree needs the wxInspector dependency, which upstream
added in the interim (python3 and wxWidgets 3.3.2 were already present
in the shared deps prefix).
* fix tree support brim
* treesupport3d part 1: more diagnostic logging. (todo once things are fixed: remove this / gate it properly)
* make area under Z=0 in rotated slice pipeline not solid
* fix solid Z=0 layer for belt printers
* fix renderer
* clean up logging
* final review pass
# Description
<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
> * What issue does this PR address or fix?
> * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The Cartesian designed-view preview over-extended the toolpaths past the model
shell by a height-proportional amount (up to ~20mm tall parts), most visibly on
long multi-part prints; compact parts like a calibration cube looked fine.
Two coupled causes:
- Belt start G-code that primes with a Z advance and a 'G92 Z0' reset leaves a
constant machine-Z origin in the GCodeProcessor, so move positions are stored as
gcode_Z + origin. The linear back-transform mixes that constant with the
gantry-Y term, leaving a per-move designed-Y error that min-corner anchoring
cannot cancel when an elevated move (e.g. a bridge) happens to cancel it at the
bbox minimum. Expose GCodeProcessorResult::belt_z_origin (the m_origin[Z] left by
the start G-code) and subtract it before the back-transform.
- Elevated features (bridges/overhangs) are mis-mapped by the linear inverse to
outside the model body; build the anchor bbox only from moves within model_bb +/-
10mm, with a fallback to the full bbox when the clip would drop the bulk (object
placed away from the belt entry) so the gross-offset case still anchors.
Preview-only; G-code output is unchanged.
The belt designed (upright) preview back-transforms the machine-frame G-code
into model space with the linear belt inverse. That inverse recovers the
print's shape and orientation, but not the per-object placement/lift
translation: the object's position on the belt, the BeltSliceStrategy min-Z
lift, and the centering pre-translate are applied OUTSIDE
build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse
cannot undo them. The result was a constant offset (~20 mm on the belt-advance
axis) of the toolpaths from the model shell, on every model.
Recover the missing translation generally — independent of the offset's exact
source or the axis remap — by anchoring the back-transformed object body
(extrusions on layer_id >= 1, i.e. excluding the layer-0 prime/skirt) onto the
upright model bounding box, the same space the shells render in, and folding
that translation into the belt inverse before converting to libvgcode.
Replaces the previous Y=0 anchoring in LibVGCodeWrapper, which pinned the
toolpaths to the belt entry rather than to the model and so left the offset in
place for any object not sitting at the origin.
On a belt printer the emitted G-code is in the machine frame (45-deg sheared,
axis-remapped, scaled), so the toolpath preview shows the print as a sheared
slab floating off the bed. Map each toolpath vertex back to model/Cartesian
space for the "designed" view.
The back-transform is the inverse of the full G-code forward pipeline
(BeltGCodeWriter::to_machine_coords):
model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
built from config, so it handles any rotation / shear / scale / axis-remap
combination, not just plain 45-deg belt slicing. Computed in load_as_gcode()
from print.config() and applied per-vertex inside libvgcode::convert (display
position only; layer_id, times and the volumetric/flow math keep the raw
machine values, so the layer slider and stats are unaffected).
- Toggle with the existing "Show designed view" checkbox / hotkey B; off shows
the raw machine-frame G-code (useful for debugging the transform itself).
Defaults to on.
- Belt printers skip the same-result-id load cache so the upright view applies
and the toggle takes effect even when the G-code is unchanged.
- The object extrusions (layer_id >= 1) are anchored to the belt entry to drop
the constant machine-origin offset (start-G-code belt advance) that the linear
back-transform alone does not capture; start-G-code prime lines are excluded
so they don't steal the anchor.
Physical max-volumetric-speed test (belt #62 v4 asset) on the IR3 V2 with eSUN
PLA white: the wall stayed clean up to ~100 mm/s = ~20 mm3/s before
under-extrusion. The shipped cap of 10 mm3/s was ~half the real ceiling and
was silently throttling infill.
- eSUN PLA @IdeaFormer IR3 V2: filament_max_volumetric_speed 10 -> 20
- 0.20mm Standard @IdeaFormer IR3 V2: sparse_infill_speed 200 (~18 mm3/s at the
new cap, no longer throttled). Outer wall (45), PA (0.12), accel (1000)
unchanged — accuracy preserved.
- IdeaFormer.json version bump for profile-cache refresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Belt printers can't slice a tall vertical temperature tower. This adds a
belt-specific temperature-tower model — a row of discrete, individually
engraved provini laid along the belt, each printed at one temperature via
custom per-layer M104. Each provino is an inverted-L overhang that stresses
print quality, so the operator reads the best temperature off overhang
quality rather than a continuous ramp.
It is offered as a "Test model" choice in the temperature calibration dialog
(mirroring the Cornering test's selector), so users keep Joe's counter-rotated
sectioned tower as "Standard" and can pick this one as "Overhang":
- Calib_Params::test_model (existing field) carries the choice.
- Temp_Calibration_Dlg gets a Standard/Overhang radio.
- Plater::calib_temp belt branch: test_model 0 -> _calib_temp_belt_sectioned
(unchanged Standard path), 1 -> the discrete-provini Overhang path.
Assets: belt_temp_provino_unit.stl + belt_temp_tower_<start>_<end>.stl (6
ranges) + gen_belt_temp_tower.py (manifold engraving). Based on
belt/generic-calibrations. The Overhang path is HW-validated on the IdeaFormer
IR3 V2 (discrete M104 + engraved numbers); not re-validated since the rebase.
Enables supported printing of standard Orcaslicer calibration profiles.
* Build 2 Checkpoint
* fix support generation wedge, ghost layers
* flip cornering tests 180 deg to waste less supports
* fix row spacing on the flow ratio calibrations
* more testing, this didn't fix anything
* switched rotation tools, same issue
* fixed Z-offset issues
* add rest of PA features, may look a bit weird on a belt
* make temp towers work
* re-enable spiral on calibrations that want it
* Final cleanup pre-PR and community testing
The IdeaFormer IR3 V2 End G-code ran `G28 ; home all`, which homes the
Z (belt) and Y (gantry) axes. On a belt printer Z is the conveyor, so
homing it runs the belt all the way back to origin, dragging the finished
part back under the gantry that G28 has just lowered — the head knocks the
print (reported by an IR3 V2 user; the `G1 Y50` lift came after the G28,
too late).
Replace the end sequence with a belt-safe one: switch to relative mode
(G91), lift the gantry for clearance, advance the belt forward one full
machine-depth (Z676, the 676 mm product depth) to eject the part and cycle
the belt surface clean, then home X only — never the Z/belt axis.
collect_layers_to_print() warns (CRITICAL) when an extrusion layer sits above
the previous one with an empty gap below — the fixed-bed assumption that
material with nothing under it is floating and unprintable. On a belt printer a
*leading* empty range (the gap starts at Z=0, no prior extrusion layer) is not
floating: it is the conveyor lead-in, and the part rests on the advancing belt
as the first material is laid down well above Z=0. A part not designed for a
belt (e.g. a flat test model tilted into the belt frame) then trips this as a
false "Object can't be printed for empty layer between 0 and N" error.
Suppress only the leading case (belt_printer && last_extrusion_layer == null);
genuine internal gaps are still flagged, since on a belt those can be an
over-angle overhang printing into air. Non-belt output is unchanged.
The original PR skipped the max-print-height check entirely on belt printers
because the sliced (virtual) Z is belt travel, not build height. As the reviewer
noted, that removed the only working height guard. Restore a correct guard:
- Print::validate: on belt printers, compare the upright object height
(max over instances of the scene-space bbox) against printable_height directly.
printable_height is the usable VERTICAL clearance above the belt: the gantry
travels up the tilted plane (reach = height/cos(tilt)) and its axis range is
sized for that (IR3 V2: ~354 mm gantry travel = 250 mm vertical at 45deg, and
printable_height = 250). Hardware-confirmed 250 mm vertical clearance, so no
cos(tilt) factor is applied.
- BuildVolume::set_belt_printer: drop the diagonal Z scaling; the build-volume Z
already equals printable_height, keeping the live 'outside build volume'
highlight in agreement with validate().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Add IdeaFormer IR3 V2 belt printer profile
Self-contained vendor profile for the IdeaFormer IR3 V2 (45 deg belt printer):
machine (0.4 nozzle) + 0.20mm process + Generic PLA/PETG filaments, with the
belt machine-frame transforms set explicitly on the machine preset
(belt_printer, belt_slice_rotation x/45/global, build_plate_tilt_x=45,
gcode_remap_x/y/z, gcode_shear_z=pos_tan, gcode_scale_y=inv_cos).
The vendor bundles its own machine/process commons (fdm_belt_common,
fdm_klipper_common, fdm_machine_common, fdm_process_common) on purpose:
OrcaSlicer resolves system-preset inheritance per-vendor, so a profile that
inherits the Custom vendor's commons cross-vendor fails to resolve its parent
and the whole IdeaFormer vendor silently fails to load. Bundling the commons
(and listing them in IdeaFormer.json in dependency order) keeps the vendor
self-contained, matching how every other vendor folder is structured.
Machine limits, bed temperature (75 C for belt PLA) and start/end G-code are
taken from a working IdeaFormer IR3 V2.
* feat(belt/profile): eSUN PLA @IdeaFormer IR3 V2 — HW-calibrated belt filament
Add an eSUN PLA belt profile for the IR3 V2, inheriting Generic PLA @IdeaFormer
IR3 V2 (self-contained: parent is in the same IdeaFormer vendor, registered
after it in filament_list). HW-calibrated on the IR3 V2:
- nozzle_temperature 200/200 (temp-tower calibration)
- pressure_advance 0.12 (PA calibration)
- filament_max_volumetric_speed 10 mm³/s (max-vol-speed calibration: wall
failed at 126 mm/s → 126 × 0.0798 mm³/mm ≈ 10 mm³/s)
* fix: restore BuildVolume bounds when toggling belt mode
set_belt_printer() mutated m_bboxf when enabling but never restored
the original extents on disable or when switching infinite_y true->false,
leaving stale max.y/max.z values that broke collision and object_state
checks. Recompute m_bboxf from m_bed_shape + m_max_print_height at the
top of each call, then apply belt-specific adjustments on top.
Addresses Copilot review comment on PR #12998 (BuildVolume.cpp:196).
* chore: drop [BELT-DEBUG] to_machine_coords log to trace
Was emitting at warning level once per 0.2mm Z bucket during every belt
print export, polluting default user logs. Trace level matches the rest
of the belt diagnostics and is silent in production.
Addresses Copilot review comment on PR #12998 (BeltGCodeWriter.cpp:86).
* chore: drop [BELTRACE] make_perimeters/support logs to trace
Eight warning-level traces around make_perimeters and
generate_support_material were emitting on every call/exit during normal
slicing, cluttering default logs. They're concurrency-debug breadcrumbs
not user-facing diagnostics, so drop them to trace.
Addresses Copilot review comment on PR #12998 (PrintObject.cpp:438).
* perf: gate BeltSliceStrategy diagnostic bbox tracking behind compile flag
apply_to_trafo() walked every model vertex twice (once for min_z, once
for per-volume mesh/slicer bboxes) and emitted seven trace logs per
call. The bboxes and logs are diagnostic only; min_z is the load-bearing
output. Wrap the bbox accumulation, logging, and supporting headers in
SLIC3R_BELT_DIAGNOSTIC_LOG so production builds do the bare min_z scan.
Addresses Copilot review comment on PR #12998 (BeltSliceStrategy.cpp:95).
* fix: apply part_cooling_fan_min_pwm to first-layer plane fan crossings
apply_first_layer_plane_fan_eval emitted band-crossing M106 commands
through GCodeWriter::set_fan() without the per-printer PWM floor that
every other set_fan call in CoolingBuffer applies. On printers with a
non-zero part_cooling_fan_min_pwm, fans could fail to spin up at low
requested speeds near the belt surface.
Addresses Copilot review comment on PR #12998 (CoolingBuffer.cpp:1227).
* initial commit
* fix upper bounds for assemblies
* significantly less Z shift issues, still not quite tamped down yet though
* add instrumentation to logs
* finally found the issue
* update printer defaults
* initial commit
* fix upper bounds for assemblies
* significantly less Z shift issues, still not quite tamped down yet though
* add instrumentation to logs
* finally found the issue
* update printer defaults
* clean up UI elements
* further cleaning
* final cleanup for first round of settings UI streamlining
* update generic belt printer settings
* fix generic again
Reconciles the belt-printer branch with upstream PRs through #13723. Six
files had conflicts; three additional files needed manual follow-up fixes
where the auto-merge produced code that referenced upstream-renamed fields
or changed function signatures.
Notable reconciliations:
- TreeSupport.cpp: kept belt-floor early-exit branches around HEAD's
drop-down logic, folded upstream's `(distance_to_top > 0 ? 1 : 0)`
formula into the non-belt-floor path (upstream PR #11812). Dropped dead
`roof_enabled`/`force_tip_to_roof` locals.
- TreeSupport3D.cpp: combined upstream's safety-offset + remove_small
changes with HEAD's belt-floor clip in the per-slice trim loop. Dropped
HEAD's `else` block (superseded by upstream's rewritten bottom-contact
propagation) and re-added the belt-floor clip into the new propagation
loop. Gated the propagation on belt printers to prevent OOM when
belt-floor clipping produces empty initial slices.
- TriangleSelector.{cpp,hpp}: merged both new `select_patch` parameters
(HEAD's `up_direction` and upstream's `select_partially`); body uses
`dot(up_direction)` for the overhang angle check and forwards
`select_partially` to `select_triangle`.
- SupportMaterial.cpp: `slicing_params.soluble_interface` →
`zero_gap_interface_bottom` in HEAD's `detect_belt_floor_bottom_contacts`,
matching upstream's same-purpose rename at line 2495.
- Custom.json, GCodeWriter.cpp: simple additive merges (kept entries /
includes from both sides).
Verified by building OrcaSlicer (RelWithDebInfo) after a full deps
rebuild (Eigen v5.0.1, libigl v2.6.0 are now managed deps) and slicing
a scaled Benchy on the NORMALIZER belt-printer profile without OOM.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* minor logic swap
* first attempt, has a race condition
* fixed the offset issue
* found a solution, I think things work now (at least once I quash this race condition)
* still chasing down race conditions
* add manual shear / scale order strategy swap
* tweak manual shear, fix ui uninitialization crash
* fix z height / g-code desync issue
* fix shear then scale cutoff planes
* getting closer
* fix support termination planes
* fix incorrect offsets in shear-then-scale mode
* test - fix overextrusion due to model/layer scale
Conflicts resolved in src/libslic3r/GCode.cpp and src/slic3r/GUI/GUI_Factories.cpp.
GCode.cpp: combined upstream's air-filtration per-extruder gating
(activate_air_filtration_during_print / _on_completion), the new
extrusion-role-change gcode lambda, ZAA's path.z_contoured arc-fit
disable, raft-aware slow_down_layers branch, and Vec3d/Line3 ZAA
plumbing with the local belt-printer changes (path_on_first_layer,
effective_layer_index_for_point, should_disable_arc_fitting). All
auto-merged m_writer.X() calls converted to m_writer->X() to match
the local unique_ptr<GCodeWriter> refactor.
GUI_Factories.cpp: inserted brim_flow_ratio in the Support category
list and renumbered around the local build_plate_tilt_x/y entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add BeltBackTransform class that inverts the shear/scale matrix and
applies it in GCodeWriter::to_machine_coords() so G-code outputs in
the machine's physical coordinate space, gated by new
belt_gcode_back_transform config option
- Extend belt floor clipping to all three tree support pipelines
(Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon
clipping, anti-overhang integration, and belt raft extension layers
- Fix tree drop_nodes() belt termination, organic support global Z
offset, collision calculation index bug, and first-layer brim/empty
layer checks for belt printers
two-shot - first build built but didn't plumb to UI. Woah.
add pre-slice axis remap, because Y needs to be Z
going to change tactic and move based on bbox min
switch to per axis snapping
per axis swap snap now per object
build plate tilt wasn't invalidating slicer settings
support upper bound now correct, need to get lower bound corrected
axis swapped support termination corrected
Z Shear works with and without pre-slice remap now
- Fix support clipping z-shift calculation by removing coordinate-space
mismatch and sync belt_floor_z_shift with global_z_offset; fix
invalidation so posSupportMaterial no longer resets slicing params
- Add belt floor polygon clipping to non-organic tree support
(slim/strong/hybrid) with collision surface integration in
TreeSupportData, belt extension layers, and first-layer brim
suppression
- Add belt floor clipping to organic tree support pipeline with virtual
belt raft layers, per-layer polygons in TreeModelVolumes, and
post-generation layer trimming; fix pre-existing processing_last_mesh
bug in calculateCollision()
Fix belt floor support clipping: z-shift, invalidation, and global offset
- Fix support clipping z-shift calculation by removing coordinate-space
mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and
sync belt_floor_z_shift with global_z_offset in global shear mode
- Fix invalidation so posSupportMaterial no longer resets slicing params,
preventing the exact posSlice z-shift from being overwritten by the
bounding-box approximation on support-only setting changes
- Remove double-counting of global z_offset on support layers — support
already inherits the offset from object layers during generation
This Work Was Co-Authored-By Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced
Fix mesh clipping through build plate after belt shear/scale transform
Generalize G-code viewer designed-view toggle for full belt transform
Clip support layers to transformed belt floor plane
Supports below the tilted build plate (Z = shear_factor * from_axis - min_z)
are now clipped via half-plane intersection after generation. Belt floor
parameters stored in SlicingParameters and populated in both update_slicing_parameters()
and the static slicing_parameters() overload.
Make belt G-code viewer toggle more prominent, add B keyboard shortcut
- Add separator + teal "Belt Printer" header in legend panel
- Append [B] hint to checkbox label
- Add B key shortcut in GLCanvas3D to toggle designed/machine view
- Read belt_printer_angle from loaded G-code headers to enable belt view
Add per-axis global transform option for belt printer shear
New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates
instance shift so objects at different bed positions get position-aware
transform (Z += factor * instance_shift_on_from_axis).
Fix global shear: use layer Z offset instead of mesh transform, add config invalidation
- Global shear offset applied as post-slicing layer print_z adjustment
instead of mesh transform (which was absorbed by min_z normalization
or shifted mesh out of slice range)
- Register all belt transform options in Print::invalidate_state_by_config_options
to trigger posSlice re-slicing (the fallback only invalidated Print steps,
not PrintObject steps — belt changes had no effect without manual re-slice)
- Belt gcode remap options added to steps_gcode (gcode-export only)
- Skip empty-first-layer check for belt objects with global Z offset
WIP: split instances for global shear, relative Z offsets, debug logging
- PrintApply: when belt global mode active, prevent instance grouping by
adding unique Z perturbation to trafo — each copy becomes its own
PrintObject with independent layers
- PrintObjectSlice: compute global Z offset relative to minimum Y shift
across all PrintObjects (lowest-Y object stays at Z=0)
- Debug logging (warning level) for belt global shift values and offsets
Known issues:
- Cached posSlice results cause stale offsets when mixing copies with
individually-added objects — need to compute min baseline outside slice()
- Supports still generate to Z=0 instead of object's global Z offset
Fix global shear for copied objects: disable shared-object layer optimization
When belt global Z shear is active, each object needs unique layer Z
values based on its bed position. The shared-object optimization was
causing copies to reuse the source object's layers (and its Z offset)
instead of computing their own position-based offset.
started work on getting supports to work properly
one step forward, one step back
this version didn't quite work. Getting somewhere though
about to add UI controllable tests
added configuration options for supports
tweak CLAUDE.md to be more aggressive for my machine. This commit should probably be pulled out before contributing upstream
still chasing down some bugs
moving objects between slices no longer results in improper Z-height because of caching
added more data to the debug logs
Z offset is getting more global again
still not quite there, I think there's a fundamental logic flaw?
hunting for bugs
finally have a functional fix
Add belt floor clipping to tree supports (organic and non-organic)
- Add belt floor polygon clipping to non-organic tree support
(slim/strong/hybrid) in draw_circles() and terminate nodes at the
belt surface instead of the horizontal build plate
- Add belt floor clipping to organic tree support pipeline with virtual
belt raft layers for sub-floor branch generation, per-layer belt
floor polygons in TreeModelVolumes, and post-generation layer trimming
- Fix pre-existing processing_last_mesh bug in TreeModelVolumes that
prevented m_anti_overhang (support blockers) from ever being applied;
skip empty first layer check for belt printers
Commits:
current approach: make a face surface to build supports to
closer!
supports now terminate on shear plane, now need to get shear plane to correct Z height
nearly there
chasing down logic issues still
committing for checkpoint, this still does not work
still got logic problems...
cull support clipping
stashing changes for now. Going to focus on getting the global shear OFF support generation dialed first.
beginning per object shear calcs
Local shear transform is on correct Z offset now
local shear finally works now and needs more testing
global shear works now, needs thorough testing
debugging non-45 degree angles
debugging part 2
supports at all angles work now
remove debug logging
Add belt floor collision to non-organic tree support pipeline
- Integrate belt floor as a collision surface in TreeSupportData so
branches route around the belt naturally, replacing the explicit
termination checks in drop_nodes()
- Add belt extension layers below the object after draw_circles() to
allow support geometry to extend to the diagonal belt surface instead
of terminating at a horizontal first layer
- Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds
int32), skip first-layer brim expansion for belt printers, and
extend empty first layer check bypass to all belt modes
add debug logging, Z translate for tree supports
still not seeing any cutoff surface yet
adding debug options
attempt #2 at trees
if hit Z buildplate stop but don't set to_buildplate true
getting closer
tree support almost there, just need to get rid of the circles at the beginning
getting closer
belt / shear plane clip works, need to figure out the buidlplate plane issues
more logic, added debugging logs
supports now extend somewhat below Z=0 in global shear mode
fix bad alloc, add 10mm below build plate
fully works now
shear transform + prusa tree support generation works now.
pull out debug logging
- Implement per-object global shear transform in PrintObject with
layer Z-offset calculation, config invalidation, and fix for
shared-object layer optimization breaking copied objects
- Clip support layers to the transformed belt floor plane and begin
work on tree support adaptation for sheared coordinate space
- Improve belt UI: gray out inactive sub-options, add B keyboard
shortcut for G-code viewer design-view toggle, fix mesh clipping
through build plate after shear/scale transform
y' = y + z·cot(α),
while x' = x and z' = z
getting closer to customizable variant
getting closer
X/Y/Z shear initial
clean up UI
add 1/sin(a) transform, idea taken from blackbelt cura plugin
Things work now (turns out I've been using the wrong set of transforms)
- Replace monolithic belt rotation transform with independent per-axis
shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis
remapping, giving full flexibility to match any belt printer's
coordinate system
- Remove all rotation mode logic and intermediate type+axes dropdowns,
simplifying the pipeline to pure shear matrices while preserving the
default behavior (Y += Z*cot(45deg) with identity remap)
- Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new
shear-only model; expose 12 new settings in printer UI via
Tab.cpp/Preset.cpp
Implement belt printer tilted slicing
Implement the core belt slicing pipeline that makes the slicer
tilt-aware:
Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation
from slicing frame to machine frame
Step 2: PrintObject - belt-rotated object height calculation
(y*sin(a) + z*cos(a)) for correct layer count
Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so
horizontal slice planes correspond to belt-parallel planes,
with Z-shift computed from model volumes
Step 4: GCodeProcessor - machine-frame preview (no transform needed)
Step 5: 3DBed - rotate bed visualization about X by belt angle
Fix: belt surface IS the build plate, no mesh rotation
Currently still slicing perpendicular to the belt normal. Need to figure out why.
Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt
The previous R(+alpha, X) transform produced negative Z values
(-y*sin(a) term dominated). Changed to R(-alpha, X) which gives
machine_z = y*sin(a) + z*cos(a), always positive for points
above the belt surface. Z increases with each layer as expected.
reverting and changing slice methodology
Add pink slicing direction arrow from origin
Shows the effective slicing direction (gantry normal) as a pink
arrow from the origin. Shorter and wider than the gravity arrow.
Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is
the layer stacking direction in the original mesh frame.
Fix slicing arrow visibility and add raw G-code toggle
- Disable depth test for pink slicing arrow so it renders on top of
the tilted bed geometry (was being occluded)
- Remove unnecessary 5mm Z-offset from arrow position
- Add m_belt_show_raw toggle to GCodeViewer
- Add "Show raw G-code (slicing frame)" checkbox in legend when
belt mode is active
Implement to_machine_coords inverse rotation for belt printer G-code
The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to
start at 0. The G-code output now undoes this transform via
to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the
original machine-frame coordinates where Y is horizontal and Z is
vertical.
Changes:
- GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift
- GCodeWriter: add belt_z_shift member and setter/getter
- GCode.cpp: compute Z-shift from print objects (same logic as
PrintObjectSlice) and pass to writer; write z_shift to G-code header
- GCodeProcessor: parse belt_z_shift from G-code header
- GCodeViewer: store belt_z_shift from processor result
Wire raw G-code toggle to apply slicing-frame view transform
When "Show raw G-code (slicing frame)" is checked in the preview
legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift)
to the toolpath rendering. This shows the G-code as it was during
slicing: rotated part with horizontal layers.
Default (unchecked): machine-frame view — upright part with tilted layers.
Remove belt printer placeholder comment from GCodeProcessor
The preview now correctly displays machine-frame G-code with the
optional raw view toggle. No transform is needed in the processor.
- Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count
Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode,
GCodeProcessor, and GCodeViewer
Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views
This is a combination of 6 commits.
checkpoint 1: initial MVP. Slicing functions, but rotates instead of skews are happening and a lot of other stuff too
getting somewhere, getting to the point where I need to figure out how to verify this stuff
this appears to be a dead end.
getting somewhere I think maybe
I'm pretty sure we've completely lost the plot at this point and need to restart this process...
remove slice logic in preparation for new, more invasive plan
- Add BeltBackTransform class that inverts the shear/scale matrix and
applies it in GCodeWriter::to_machine_coords() so G-code outputs in
the machine's physical coordinate space, gated by new
belt_gcode_back_transform config option
- Extend belt floor clipping to all three tree support pipelines
(Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon
clipping, anti-overhang integration, and belt raft extension layers
- Fix tree drop_nodes() belt termination, organic support global Z
offset, collision calculation index bug, and first-layer brim/empty
layer checks for belt printers
two-shot - first build built but didn't plumb to UI. Woah.
add pre-slice axis remap, because Y needs to be Z
going to change tactic and move based on bbox min
switch to per axis snapping
per axis swap snap now per object
build plate tilt wasn't invalidating slicer settings
support upper bound now correct, need to get lower bound corrected
axis swapped support termination corrected
Z Shear works with and without pre-slice remap now
- Fix support clipping z-shift calculation by removing coordinate-space
mismatch and sync belt_floor_z_shift with global_z_offset; fix
invalidation so posSupportMaterial no longer resets slicing params
- Add belt floor polygon clipping to non-organic tree support
(slim/strong/hybrid) with collision surface integration in
TreeSupportData, belt extension layers, and first-layer brim
suppression
- Add belt floor clipping to organic tree support pipeline with virtual
belt raft layers, per-layer polygons in TreeModelVolumes, and
post-generation layer trimming; fix pre-existing processing_last_mesh
bug in calculateCollision()
Fix belt floor support clipping: z-shift, invalidation, and global offset
- Fix support clipping z-shift calculation by removing coordinate-space
mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and
sync belt_floor_z_shift with global_z_offset in global shear mode
- Fix invalidation so posSupportMaterial no longer resets slicing params,
preventing the exact posSlice z-shift from being overwritten by the
bounding-box approximation on support-only setting changes
- Remove double-counting of global z_offset on support layers — support
already inherits the offset from object layers during generation
This Work Was Co-Authored-By Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced
Fix mesh clipping through build plate after belt shear/scale transform
Generalize G-code viewer designed-view toggle for full belt transform
Clip support layers to transformed belt floor plane
Supports below the tilted build plate (Z = shear_factor * from_axis - min_z)
are now clipped via half-plane intersection after generation. Belt floor
parameters stored in SlicingParameters and populated in both update_slicing_parameters()
and the static slicing_parameters() overload.
Make belt G-code viewer toggle more prominent, add B keyboard shortcut
- Add separator + teal "Belt Printer" header in legend panel
- Append [B] hint to checkbox label
- Add B key shortcut in GLCanvas3D to toggle designed/machine view
- Read belt_printer_angle from loaded G-code headers to enable belt view
Add per-axis global transform option for belt printer shear
New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates
instance shift so objects at different bed positions get position-aware
transform (Z += factor * instance_shift_on_from_axis).
Fix global shear: use layer Z offset instead of mesh transform, add config invalidation
- Global shear offset applied as post-slicing layer print_z adjustment
instead of mesh transform (which was absorbed by min_z normalization
or shifted mesh out of slice range)
- Register all belt transform options in Print::invalidate_state_by_config_options
to trigger posSlice re-slicing (the fallback only invalidated Print steps,
not PrintObject steps — belt changes had no effect without manual re-slice)
- Belt gcode remap options added to steps_gcode (gcode-export only)
- Skip empty-first-layer check for belt objects with global Z offset
WIP: split instances for global shear, relative Z offsets, debug logging
- PrintApply: when belt global mode active, prevent instance grouping by
adding unique Z perturbation to trafo — each copy becomes its own
PrintObject with independent layers
- PrintObjectSlice: compute global Z offset relative to minimum Y shift
across all PrintObjects (lowest-Y object stays at Z=0)
- Debug logging (warning level) for belt global shift values and offsets
Known issues:
- Cached posSlice results cause stale offsets when mixing copies with
individually-added objects — need to compute min baseline outside slice()
- Supports still generate to Z=0 instead of object's global Z offset
Fix global shear for copied objects: disable shared-object layer optimization
When belt global Z shear is active, each object needs unique layer Z
values based on its bed position. The shared-object optimization was
causing copies to reuse the source object's layers (and its Z offset)
instead of computing their own position-based offset.
started work on getting supports to work properly
one step forward, one step back
this version didn't quite work. Getting somewhere though
about to add UI controllable tests
added configuration options for supports
tweak CLAUDE.md to be more aggressive for my machine. This commit should probably be pulled out before contributing upstream
still chasing down some bugs
moving objects between slices no longer results in improper Z-height because of caching
added more data to the debug logs
Z offset is getting more global again
still not quite there, I think there's a fundamental logic flaw?
hunting for bugs
finally have a functional fix
Add belt floor clipping to tree supports (organic and non-organic)
- Add belt floor polygon clipping to non-organic tree support
(slim/strong/hybrid) in draw_circles() and terminate nodes at the
belt surface instead of the horizontal build plate
- Add belt floor clipping to organic tree support pipeline with virtual
belt raft layers for sub-floor branch generation, per-layer belt
floor polygons in TreeModelVolumes, and post-generation layer trimming
- Fix pre-existing processing_last_mesh bug in TreeModelVolumes that
prevented m_anti_overhang (support blockers) from ever being applied;
skip empty first layer check for belt printers
Commits:
current approach: make a face surface to build supports to
closer!
supports now terminate on shear plane, now need to get shear plane to correct Z height
nearly there
chasing down logic issues still
committing for checkpoint, this still does not work
still got logic problems...
cull support clipping
stashing changes for now. Going to focus on getting the global shear OFF support generation dialed first.
beginning per object shear calcs
Local shear transform is on correct Z offset now
local shear finally works now and needs more testing
global shear works now, needs thorough testing
debugging non-45 degree angles
debugging part 2
supports at all angles work now
remove debug logging
Add belt floor collision to non-organic tree support pipeline
- Integrate belt floor as a collision surface in TreeSupportData so
branches route around the belt naturally, replacing the explicit
termination checks in drop_nodes()
- Add belt extension layers below the object after draw_circles() to
allow support geometry to extend to the diagonal belt surface instead
of terminating at a horizontal first layer
- Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds
int32), skip first-layer brim expansion for belt printers, and
extend empty first layer check bypass to all belt modes
add debug logging, Z translate for tree supports
still not seeing any cutoff surface yet
adding debug options
attempt #2 at trees
if hit Z buildplate stop but don't set to_buildplate true
getting closer
tree support almost there, just need to get rid of the circles at the beginning
getting closer
belt / shear plane clip works, need to figure out the buidlplate plane issues
more logic, added debugging logs
supports now extend somewhat below Z=0 in global shear mode
fix bad alloc, add 10mm below build plate
fully works now
shear transform + prusa tree support generation works now.
pull out debug logging
- Implement per-object global shear transform in PrintObject with
layer Z-offset calculation, config invalidation, and fix for
shared-object layer optimization breaking copied objects
- Clip support layers to the transformed belt floor plane and begin
work on tree support adaptation for sheared coordinate space
- Improve belt UI: gray out inactive sub-options, add B keyboard
shortcut for G-code viewer design-view toggle, fix mesh clipping
through build plate after shear/scale transform
y' = y + z·cot(α),
while x' = x and z' = z
getting closer to customizable variant
getting closer
X/Y/Z shear initial
clean up UI
add 1/sin(a) transform, idea taken from blackbelt cura plugin
Things work now (turns out I've been using the wrong set of transforms)
- Replace monolithic belt rotation transform with independent per-axis
shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis
remapping, giving full flexibility to match any belt printer's
coordinate system
- Remove all rotation mode logic and intermediate type+axes dropdowns,
simplifying the pipeline to pure shear matrices while preserving the
default behavior (Y += Z*cot(45deg) with identity remap)
- Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new
shear-only model; expose 12 new settings in printer UI via
Tab.cpp/Preset.cpp
Implement belt printer tilted slicing
Implement the core belt slicing pipeline that makes the slicer
tilt-aware:
Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation
from slicing frame to machine frame
Step 2: PrintObject - belt-rotated object height calculation
(y*sin(a) + z*cos(a)) for correct layer count
Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so
horizontal slice planes correspond to belt-parallel planes,
with Z-shift computed from model volumes
Step 4: GCodeProcessor - machine-frame preview (no transform needed)
Step 5: 3DBed - rotate bed visualization about X by belt angle
Fix: belt surface IS the build plate, no mesh rotation
Currently still slicing perpendicular to the belt normal. Need to figure out why.
Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt
The previous R(+alpha, X) transform produced negative Z values
(-y*sin(a) term dominated). Changed to R(-alpha, X) which gives
machine_z = y*sin(a) + z*cos(a), always positive for points
above the belt surface. Z increases with each layer as expected.
reverting and changing slice methodology
Add pink slicing direction arrow from origin
Shows the effective slicing direction (gantry normal) as a pink
arrow from the origin. Shorter and wider than the gravity arrow.
Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is
the layer stacking direction in the original mesh frame.
Fix slicing arrow visibility and add raw G-code toggle
- Disable depth test for pink slicing arrow so it renders on top of
the tilted bed geometry (was being occluded)
- Remove unnecessary 5mm Z-offset from arrow position
- Add m_belt_show_raw toggle to GCodeViewer
- Add "Show raw G-code (slicing frame)" checkbox in legend when
belt mode is active
Implement to_machine_coords inverse rotation for belt printer G-code
The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to
start at 0. The G-code output now undoes this transform via
to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the
original machine-frame coordinates where Y is horizontal and Z is
vertical.
Changes:
- GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift
- GCodeWriter: add belt_z_shift member and setter/getter
- GCode.cpp: compute Z-shift from print objects (same logic as
PrintObjectSlice) and pass to writer; write z_shift to G-code header
- GCodeProcessor: parse belt_z_shift from G-code header
- GCodeViewer: store belt_z_shift from processor result
Wire raw G-code toggle to apply slicing-frame view transform
When "Show raw G-code (slicing frame)" is checked in the preview
legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift)
to the toolpath rendering. This shows the G-code as it was during
slicing: rotated part with horizontal layers.
Default (unchecked): machine-frame view — upright part with tilted layers.
Remove belt printer placeholder comment from GCodeProcessor
The preview now correctly displays machine-frame G-code with the
optional raw view toggle. No transform is needed in the processor.
- Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count
Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode,
GCodeProcessor, and GCodeViewer
Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views
This is a combination of 6 commits.
checkpoint 1: initial MVP. Slicing functions, but rotates instead of skews are happening and a lot of other stuff too
getting somewhere, getting to the point where I need to figure out how to verify this stuff
this appears to be a dead end.
getting somewhere I think maybe
I'm pretty sure we've completely lost the plot at this point and need to restart this process...
remove slice logic in preparation for new, more invasive plan
"default_print_profile":"0.20mm Standard @IdeaFormer IR3 V2",
"use_relative_e_distances":"1",
"machine_max_acceleration_extruding":[
"5000",
"5000"
],
"machine_max_acceleration_retracting":[
"1000",
"1000"
],
"machine_max_acceleration_travel":[
"9000",
"9000"
],
"machine_max_acceleration_x":[
"5000",
"5000"
],
"machine_max_acceleration_y":[
"5000",
"5000"
],
"machine_max_acceleration_z":[
"100",
"100"
],
"machine_max_jerk_x":[
"10",
"10"
],
"machine_max_jerk_y":[
"10",
"10"
],
"machine_max_jerk_z":[
"0.4",
"0.4"
],
"machine_max_speed_e":[
"60",
"60"
],
"machine_max_speed_x":[
"500",
"500"
],
"machine_max_speed_y":[
"500",
"500"
],
"machine_max_speed_z":[
"20",
"20"
],
"retraction_length":[
"2"
],
"retraction_speed":[
"40"
],
"deretraction_speed":[
"40"
],
"retract_lift_below":[
"300"
],
"machine_start_gcode":"; === IdeaFormer IR3 V2 Belt Printer Start ===\n; Axes: X=lateral, Y=gantry height (probe), Z=belt\nG90 ; absolute positioning\nM82 ; absolute extruder\nG21 ; millimeters\nG28 ; home all axes\nG1 Y20 F500 ; lift nozzle 20mm from belt\n; Bed + hotend temps come from the active filament profile. Belt PLA requires 75 C bed — use Generic/eSun PLA @IdeaFormer IR3 V2 filament presets to get it automatically.\nM140 S[hot_plate_temp_initial_layer] ; set bed temp\nM104 S[nozzle_temperature_initial_layer] ; hotend temp\nM109 S[nozzle_temperature_initial_layer] ; wait hotend\nM190 S[hot_plate_temp_initial_layer] ; wait bed\n; --- Purge blob ---\nG92 E0 ; zero extruder\nG1 Y.1 ; nozzle 0.1mm above belt\nG1 E15 F1000 ; purge 15mm blob\nG1 Z20 E25 F800 ; belt advance 20mm + extrude\nG1 E23 ; retract 2mm\nG28 Y ; re-probe belt surface\nG1 E25 ; de-retract\n; --- Prime lines (full 250mm bed width) ---\nFMS_on ; filament motion sensor\nG1 X250 E50 F2000 ; prime line 1\nG92 Z0 ; reset belt origin\nG1 Z.4 ; belt advance 0.4mm\nG1 X0 E75 ; prime line 2\nG1 F1000 ; default feedrate\nG92 E0 Z0 ; zero extruder + belt = print origin\n",
"machine_end_gcode":"; === IdeaFormer IR3 V2 Belt Printer End ===\nM400 ; wait for moves to finish\nM104 S0 ; heater off\nM140 S0 ; bed off\nG92 E0 ; zero extruder\nG1 E-5 F300 ; retract 5mm\nG4 P5000 ; wait for ooze\nG91 ; relative mode - keep every end move relative on a belt\nG1 Y20 F1000 ; raise gantry 20mm for clearance over the part\nG1 Z676 F3000 ; advance belt one full machine-depth to eject the part and clean the belt\nG90 ; back to absolute\nG28 X ; home X only - NEVER 'G28' all: that homes Z/belt and reverses the whole print back into the gantry\nFMS_off ; filament motion sensor off\nBED_MESH_CLEAR\nM84 ; disable motors\n",
Swiper - is the free and most modern mobile touch slider with hardware accelerated transitions and amazing native behavior. It is intended to be used in mobile websites, mobile web apps, and mobile native/hybrid apps.
Swiper is not compatible with all platforms, it is a modern touch slider which is focused only on modern apps/platforms to bring the best experience and simplicity.
# Getting Started
* [Getting Started Guide](https://swiperjs.com/get-started/)
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.