diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index a86ee98af3..95aca4bd70 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -216,6 +216,15 @@ public: void export_layer_filaments(GCodeProcessorResult* result); //BBS: set offset for gcode writer void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);} + // IMEX firmware-managed slice: writer offset is augmented by the IMEX shift so the + // emitted gcode is centered at bed origin (firmware fans copies/mirrors out from there). + // Processor offset stays at plate_origin only so the gcode-preview visualizer renders + // the centered toolpath at bed center, not at the prepare-view zone placement. When + // imex_x/imex_y are zero this is byte-identical to set_gcode_offset(x, y). + void set_gcode_offset_with_imex_shift(double x, double y, double imex_x, double imex_y) { + m_writer.set_xy_offset(x + imex_x, y + imex_y); + m_processor.set_xy_offset(x, y); + } // Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests. const Vec2d& origin() const { return m_origin; } diff --git a/src/libslic3r/IMEXHelpers.cpp b/src/libslic3r/IMEXHelpers.cpp index 21a704cfbd..878e8433b0 100644 --- a/src/libslic3r/IMEXHelpers.cpp +++ b/src/libslic3r/IMEXHelpers.cpp @@ -388,4 +388,15 @@ Transform3d imex_head_transform(int /*primary*/, int /*target*/, ImexRole role, return Transform3d::Identity(); } +Vec2d compute_imex_slice_offset(bool firmware_managed, + const std::string& parallel_mode, + const std::optional& primary_zone_box) +{ + if (!firmware_managed) return Vec2d::Zero(); + if (parallel_mode.empty()) return Vec2d::Zero(); + if (parallel_mode == kImexPrimaryMode) return Vec2d::Zero(); + if (!primary_zone_box.has_value()) return Vec2d::Zero(); + return primary_zone_box->center(); +} + } // namespace Slic3r diff --git a/src/libslic3r/IMEXHelpers.hpp b/src/libslic3r/IMEXHelpers.hpp index 7b58ba8933..e2da46b454 100644 --- a/src/libslic3r/IMEXHelpers.hpp +++ b/src/libslic3r/IMEXHelpers.hpp @@ -1,11 +1,13 @@ #pragma once #include +#include #include #include #include #include +#include "libslic3r/BoundingBox.hpp" #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Point.hpp" @@ -289,4 +291,13 @@ Transform3d imex_head_transform(int primary, int target, ImexRole role, const Vec2d& gantry_offset, const Vec2d& primary_zone_center = Vec2d::Zero()); +// Slice-time XY shift for printers that delegate copy/mirror placement to firmware. +// When `imex_firmware_managed_zones` is on AND the active mode is non-primary, returns +// the primary zone's plate-local center so gcode emission can subtract it (yielding a +// centered slice the firmware can fan out). Returns Vec2d::Zero() in every other case: +// flag off, primary/empty mode, or no zone box (IMEX disabled, primary-only, etc.). +Vec2d compute_imex_slice_offset(bool firmware_managed, + const std::string& parallel_mode, + const std::optional& primary_zone_box); + } // namespace Slic3r diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index c0b7a06d20..2ed3471eeb 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1354,7 +1354,8 @@ static std::vector s_Preset_printer_options { "disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut", "bed_temperature_formula", "nozzle_flush_dataset", // IDEX/IQEX (independent X extruder) — printer capability and user-defined modes - "is_imex", "imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout", + "is_imex", "imex_firmware_managed_zones", + "imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout", "imex_nozzle_clearance_x", "imex_nozzle_clearance_y", "imex_carriage_margin", "imex_viz_theme", "imex_mode_names", "imex_mode_active_tools", "imex_mode_gcodes" }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 65a7ed6207..ec11935656 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -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) diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index eeb74a87ed..73fe356a7b 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -1040,6 +1040,12 @@ public: //BBS: plate's origin related functions void set_plate_origin(Vec3d origin) { m_origin = origin; } const Vec3d get_plate_origin() const { return m_origin; } + // IMEX firmware-managed zones: additional XY shift applied at gcode emission so the + // primary zone's contents land at bed origin (0,0). Set by the GUI/PartPlate slice + // handoff when `imex_firmware_managed_zones` is on AND the active mode is non-primary. + // Vec2d::Zero() in every other case → byte-identical gcode output. + void set_imex_slice_offset(const Vec2d& o) { m_imex_slice_offset = o; } + Vec2d get_imex_slice_offset() const { return m_imex_slice_offset; } //BBS: export gcode from previous gcode file from 3mf void set_gcode_file_ready(); void set_gcode_file_invalidated(); @@ -1166,6 +1172,8 @@ private: //BBS: plate's origin Vec3d m_origin; + // IMEX firmware-managed slice offset (plate-local primary-zone center, or zero). + Vec2d m_imex_slice_offset { Vec2d::Zero() }; //BBS: modified_count int m_modified_count {0}; //BBS diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index faabf3fd73..ad1eaf5d56 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -5620,6 +5620,21 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionBool(false)); + // Firmware-managed zones: slice the primary zone's contents at bed origin (0,0) + // so a center-origin printer firmware can apply its own copy/mirror offsets to + // each toolhead. The slicer's zone overlay, placement check, and mode signaling + // are unchanged; only the gcode emission frame shifts. + def = this->add("imex_firmware_managed_zones", coBool); + def->label = L("Firmware-managed zones (center-origin slice)"); + def->tooltip = L("When enabled, the slicer emits g-code with the primary zone's " + "contents centered on bed origin (0,0). The printer firmware is " + "responsible for placing copies/mirrors at each physical zone " + "position. Enable only on printers whose firmware applies its own " + "zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX " + "duplication mode). Has no effect in Primary mode."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("imex_gantry_count", coInt); def->label = L("Gantry Count"); def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows)."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a7456c6943..af8d09c986 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1370,6 +1370,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionBool, manual_filament_change)) // IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers ((ConfigOptionBool, is_imex)) + ((ConfigOptionBool, imex_firmware_managed_zones)) ((ConfigOptionInt, imex_gantry_count)) ((ConfigOptionInt, imex_tools_per_gantry)) ((ConfigOptionFloat, imex_nozzle_clearance_x)) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 7a699ea2c1..b9761ef3bd 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1089,6 +1089,17 @@ void PartPlate::calc_imex_ghosts() int primary_phys = -1; if (!resolve_active_mode_tools(active_tools_str, primary_phys)) return; + // IMEX firmware-managed zones: the slicer emits a centered single-half slice and the + // firmware fans copies/mirrors out from there — placement is not slicer-authoritative. + // Rendering secondary-tool ghosts via imex_head_transform (which assumes slicer-managed + // placement at primary_zone_center + gantry_offset) would draw them at positions the + // firmware doesn't honor (e.g. off-bed once the centered slice is in play). Suppress + // ghost generation entirely so we don't lie about something the slicer doesn't own. + if (auto* fw_opt = wxGetApp().preset_bundle->printers.get_edited_preset() + .config.option("imex_firmware_managed_zones"); + fw_opt && fw_opt->value) + return; + // Zone centers are the basis for ghost placement; make sure they exist before // we read them. render_imex_zones already ensures this, but ghost rebuild can // also be driven from mode/preset invalidation paths that don't touch zones. @@ -4507,6 +4518,35 @@ void PartPlate::update_slice_result_valid_state(bool valid) } } +// IMEX firmware-managed zones: compute the plate-local primary-zone center and push it +// to m_print as the slice-time XY shift. Vec2d::Zero() in every non-firmware-managed +// path (flag off, primary/empty mode, empty zone box) → byte-identical gcode output for +// unaffected printers. Defensive accessors tolerate option() returning nullptr in case +// the preset bundle is reached during early Plater construction. +// Called both from update_slice_context (plate switch path) and from +// Plater::priv::update_background_process (every-slice path) so reslice with mode toggled +// without plate change picks up the right shift. +void PartPlate::refresh_imex_slice_offset() +{ + Vec2d imex_off = Vec2d::Zero(); + if (auto* app = &wxGetApp(); app && app->preset_bundle && m_print) { + const DynamicPrintConfig& printer_cfg = app->preset_bundle->printers.get_edited_preset().config; + auto* fw_opt = printer_cfg.option("imex_firmware_managed_zones"); + if (fw_opt && fw_opt->value) { + std::string active_mode = get_imex_mode(); + if (active_mode == kImexPrimaryMode) { + const DynamicPrintConfig& process_cfg = app->preset_bundle->prints.get_edited_preset().config; + if (auto* mo = process_cfg.option("imex_parallel_mode")) + if (!mo->value.empty()) active_mode = mo->value; + } + ensure_imex_zones(); + imex_off = compute_imex_slice_offset(true, active_mode, m_imex_primary_zone_box); + } + } + if (m_print) + m_print->set_imex_slice_offset(imex_off); +} + //update current slice context into backgroud slicing process void PartPlate::update_slice_context(BackgroundSlicingProcess & process) { @@ -4520,6 +4560,8 @@ void PartPlate::update_slice_context(BackgroundSlicingProcess & process) wxQueueEvent(m_plater, event); }; + refresh_imex_slice_offset(); + process.set_fff_print(m_print); process.set_gcode_result(m_gcode_result); process.select_technology(this->printer_technology); diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 83bcfdd93f..872eddab19 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -522,6 +522,10 @@ public: // Returns the primary-zone bounding box in mm when an IDEX/IQEX parallel mode is active. // Empty (nullopt) when IDEX/IQEX is off or the mode is "primary" (full-bed). std::optional imex_primary_zone() { ensure_imex_zones(); return m_imex_primary_zone_box; } + // Compute the IMEX firmware-managed slice offset and push it to m_print. Safe to call + // every slice. Sets Vec2d::Zero() when the printer flag is off, the mode is primary, or + // the preset bundle isn't ready yet. Must be called from the GUI thread. + void refresh_imex_slice_offset(); // Returns carriage collision strips (mm) for the current IDEX/IQEX mode. // Always call imex_primary_zone() first to ensure the cache is warm. const std::vector& imex_collision_zones() const { return m_imex_collision_zones; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 110cbbf356..344d519cdb 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7933,6 +7933,13 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool else invalidated = background_process.apply(this->model, preset_bundle->full_config(false)); + // IMEX firmware-managed zones: refresh the per-slice XY shift on the print backend. + // Plater::reslice() calls update_background_process with switch_print=false, which skips + // update_slice_context_to_current_plate — so the offset would otherwise stay stale across + // mode changes. Pushing it here guarantees every slice picks up the current mode + zone. + if (auto* cur_plate = this->partplate_list.get_curr_plate()) + cur_plate->refresh_imex_slice_offset(); + if ((invalidated == Print::APPLY_STATUS_CHANGED) || (invalidated == Print::APPLY_STATUS_INVALIDATED)) // BBS: add only gcode mode q->set_only_gcode(false); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 238b6709fa..8961ab7ace 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5560,6 +5560,7 @@ if (is_marlin_flavor) // IDEX/IQEX (IMEX) parallel printing configuration optgroup = page->new_optgroup(L("IDEX/IQEX Configuration"), L"param_advanced"); optgroup->append_single_option_line("is_imex"); + optgroup->append_single_option_line("imex_firmware_managed_zones"); optgroup->append_single_option_line("imex_gantry_count"); optgroup->append_single_option_line("imex_tools_per_gantry"); optgroup->append_single_option_line("imex_tool_layout"); @@ -5982,6 +5983,7 @@ void TabPrinter::toggle_options() toggle_option(el, is_imex); toggle_option("imex_tool_layout", is_imex); toggle_option("imex_viz_theme", is_imex); + toggle_option("imex_firmware_managed_zones", is_imex); if (m_imex_modes_ctrl) m_imex_modes_ctrl->Show(is_imex); } wxString extruder_number; diff --git a/tests/libslic3r/test_imex_helpers.cpp b/tests/libslic3r/test_imex_helpers.cpp index 9fd7633328..e1001929d7 100644 --- a/tests/libslic3r/test_imex_helpers.cpp +++ b/tests/libslic3r/test_imex_helpers.cpp @@ -645,3 +645,76 @@ TEST_CASE("group_imex_active_tools_by_gantry — column pairing picks correct re REQUIRE(g.groups[1].gantry_index == 1); REQUIRE(g.groups[1].representative_phys == 3); // column-paired to primary T1 } + +// ----------------------------------------------------------------------------- +// compute_imex_slice_offset — center-origin shift for firmware-managed printers +// ----------------------------------------------------------------------------- +// Centered on the plate-local zone at (10,5)..(110,75) → center (60, 40). +static const BoundingBoxf kPrimaryZoneCenteredAt60_40{ + Vec2d(10.0, 5.0), Vec2d(110.0, 75.0)}; + +TEST_CASE("compute_imex_slice_offset — flag off returns zero regardless of mode", "[IMEX]") { + auto z = compute_imex_slice_offset(false, "copy", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); + + z = compute_imex_slice_offset(false, "mirror", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); + + z = compute_imex_slice_offset(false, "primary", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — primary mode returns zero even with flag on", "[IMEX]") { + auto z = compute_imex_slice_offset(true, "primary", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — empty mode returns zero even with flag on", "[IMEX]") { + auto z = compute_imex_slice_offset(true, "", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — empty zone box returns zero even with flag on and copy mode", "[IMEX]") { + auto z = compute_imex_slice_offset(true, "copy", std::nullopt); + REQUIRE_THAT(z.x(), WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — flag on + copy mode returns primary zone center", "[IMEX]") { + auto z = compute_imex_slice_offset(true, "copy", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(60.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(40.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — flag on + mirror mode returns primary zone center", "[IMEX]") { + auto z = compute_imex_slice_offset(true, "mirror", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(60.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(40.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — user-defined non-primary mode names trigger shift", "[IMEX]") { + // Mode names are user-defined; anything not "primary" or "" should activate. + auto z = compute_imex_slice_offset(true, "iq-copy", kPrimaryZoneCenteredAt60_40); + REQUIRE_THAT(z.x(), WithinAbs(60.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(40.0, 1e-9)); +} + +TEST_CASE("compute_imex_slice_offset — center value tracks zone box position", "[IMEX]") { + // Right half of a center-origin bed: (0,-75)..(160,75) → center (80, 0). + BoundingBoxf right_half{Vec2d(0.0, -75.0), Vec2d(160.0, 75.0)}; + auto z = compute_imex_slice_offset(true, "copy", right_half); + REQUIRE_THAT(z.x(), WithinAbs(80.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); + + // Left half of a center-origin bed: (-160,-75)..(0,75) → center (-80, 0). + BoundingBoxf left_half{Vec2d(-160.0, -75.0), Vec2d(0.0, 75.0)}; + z = compute_imex_slice_offset(true, "mirror", left_half); + REQUIRE_THAT(z.x(), WithinAbs(-80.0, 1e-9)); + REQUIRE_THAT(z.y(), WithinAbs(0.0, 1e-9)); +} +