feat(imex): firmware-managed zones — center slice + ghost suppression

Adds the `imex_firmware_managed_zones` printer-config key (default off) for
IDEX/IQEX printers whose firmware applies its own copy/mirror offsets in
non-primary modes (e.g. RepRapFirmware IDEX duplication mode, Flashforge
Creator Pro 2/3 Pro). For these printers the slicer needs to emit a single
centered slice at bed origin and let the firmware fan toolheads out from there;
the previous slicer-managed iMEX rendering would draw a print at the primary
zone's world position (off-bed for the firmware-fan-out paradigm).

When the flag is on and the active mode is non-primary, the slicer subtracts
the primary zone's plate-local center from the gcode emission frame. The
writer offset is augmented but the gcode-processor offset stays at plate_origin
so the gcode-preview visualizer renders the centered slice at the bed center
rather than at the prepare-view zone placement. translate_to_print_space is
augmented too so first_layer_print_min/max placeholders (consumed by user
start_gcode like Felix's M118 header) reflect the centered frame.

Slice handoff lives in PartPlate::refresh_imex_slice_offset, called from both
update_slice_context (plate switch) and Plater::priv::update_background_process
(every-slice path — reslice() goes through here with switch_print=false so the
plate-switch hook alone wouldn't fire on mode toggle).

calc_imex_ghosts early-returns in firmware-managed mode: the existing
imex_head_transform math places ghosts at primary_zone_center + gantry_offset
(slicer-managed semantics), which renders off-bed when the toolpath is being
emitted in a centered frame. Proper firmware-managed ghost rendering (showing
where copies/mirrors will actually print after firmware fan-out) is deferred.

When the flag is off, all the new code paths reduce to no-ops byte-identical to
prior behavior. Layer 1 unit tests in test_imex_helpers cover every gating path
of compute_imex_slice_offset; full ctest suite passes (247/247).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-05-13 00:55:45 -04:00
co-authored by Claude Opus 4.7
parent 493a9e96d2
commit 48c603faa5
13 changed files with 199 additions and 4 deletions
+14 -3
View File
@@ -2551,7 +2551,12 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
GCode gcode;
//BBS: compute plate offset for gcode-generator
const Vec3d origin = this->get_plate_origin();
gcode.set_gcode_offset(origin(0), origin(1));
// IMEX firmware-managed zones: writer offset gets the IMEX shift so emitted gcode is
// centered at bed origin; processor offset stays at plate_origin so the gcode-preview
// visualizer renders the centered slice at the bed center rather than the prepare-view
// zone placement. Both arms reduce to set_gcode_offset() semantics when shift is zero.
const Vec2d imex_off = this->get_imex_slice_offset();
gcode.set_gcode_offset_with_imex_shift(origin(0), origin(1), imex_off.x(), imex_off.y());
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
gcode.export_layer_filaments(result);
//BBS
@@ -2820,13 +2825,19 @@ Points Print::first_layer_wipe_tower_corners(bool check_wipe_tower_existance) co
}
//SoftFever
// "Print space" is the frame the emitted gcode uses. IMEX firmware-managed mode
// shifts that frame by `m_imex_slice_offset` (the primary zone center in plate-local
// coords) so the centered slice can be fanned out by firmware. Offset is zero in
// every other case → identical behavior for non-firmware-managed printers.
Vec2d Print::translate_to_print_space(const Vec2d &point) const {
//const BoundingBoxf bed_bbox(config().printable_area.values);
return Vec2d(point(0) - m_origin(0), point(1) - m_origin(1));
return Vec2d(point(0) - m_origin(0) - m_imex_slice_offset.x(),
point(1) - m_origin(1) - m_imex_slice_offset.y());
}
Vec2d Print::translate_to_print_space(const Point &point) const {
return Vec2d(unscaled(point.x()) - m_origin(0), unscaled(point.y()) - m_origin(1));
return Vec2d(unscaled(point.x()) - m_origin(0) - m_imex_slice_offset.x(),
unscaled(point.y()) - m_origin(1) - m_imex_slice_offset.y());
}
FilamentTempType Print::get_filament_temp_type(const std::string& filament_type)