mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 01:41:03 +00:00
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:
co-authored by
Claude Opus 4.7
parent
493a9e96d2
commit
48c603faa5
@@ -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; }
|
||||
|
||||
@@ -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<BoundingBoxf>& 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
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Geometry>
|
||||
#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<BoundingBoxf>& primary_zone_box);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1354,7 +1354,8 @@ static std::vector<std::string> 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"
|
||||
};
|
||||
|
||||
+14
-3
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).");
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<ConfigOptionBool>("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<ConfigOptionBool>("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<ConfigOptionString>("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);
|
||||
|
||||
@@ -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<BoundingBoxf> 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<BoundingBoxf3>& imex_collision_zones() const { return m_imex_collision_zones; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user