mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 01:41:03 +00:00
fix(imex): mirror across the gantry-row axis on two-gantry printers
A Mirror tool reflects across the boundary it shares with the primary's zone, and
which boundary that is depends on where the tool sits:
- Same gantry: the tools are side by side along X, so the shared boundary is
vertical and the reflection negates X. This is what single-gantry IDEX does,
and it was the only case the code modelled.
- Different gantry: the zones are stacked along Y (front strip vs rear strip),
so the shared boundary is horizontal and the reflection negates Y. The part
that comes off gantry 1 is a Y-reflection of the tool directly behind it.
imex_head_transform() hardcoded diag(-1, 1, 1) for every mirror, as its own TODO
acknowledged. Lift the axis to a caller-supplied ImexMirrorAxis; PartPlate picks it
from the tool's gantry row. Both reflections keep det = -1, so a mirrored part stays
a true mirror image rather than a 180-degree rotation, which would print the
primary's part merely turned around.
The correct axis removes two workarounds. Both ghost paths special-cased aggregated
mirrors to "drop the X reflection, translate 1:1 and bake the flip into the mesh"
because reflecting X pushed the ghost off-bed as the primary was dragged. With a Y
reflection the X translation is already zero for aggregated tools, so that falls out
for free and the special cases are deleted.
Preview markers follow the same rule, which also fixes two placement bugs:
- Mirrors reflected across a Copy tool's zone edge, falling back to the primary's
column when a row had no Copy. In iq-mirror (0:P,1:C,2:M,3:M) the front row has
no Copy, so t2 and t3 both fell back and computed the identical X — both drawn
on top of each other in t3's zone. A mirror now reflects within its own zone.
- The toolhead footprint box flipped to the far side of the nozzle for any mirror
right of the primary. That only holds for an X-axis mirror, which reverses the
carriage's orientation; a cross-gantry mirror keeps the X orientation of the
tool behind it, so its box stays on the same side.
Tests cover the cross-gantry and diagonal cases, that the axis is caller-supplied
rather than inferred from the offset vector, and that both axes are reflections
(det = -1) rather than rotations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1795,15 +1795,6 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
const int pri_zone_col = zone_col(pri_phys_col);
|
||||
const int pri_zone_row = zone_row(pri_phys_row);
|
||||
|
||||
// Pre-pass: for each physical row, find the Copy reference physical column.
|
||||
// The primary row's reference is the primary itself.
|
||||
std::map<int,int> row_copy_col; // phys_row → phys_col of copy reference
|
||||
row_copy_col[pri_phys_row] = pri_phys_col;
|
||||
for (int i = 0; i < sec_count; ++i) {
|
||||
if (sec_tool_states[sec_tool_ids[i]] == 2)
|
||||
row_copy_col[phys_row_of(sec_tool_ids[i])] = phys_col_of(sec_tool_ids[i]);
|
||||
}
|
||||
|
||||
// Primary carriage box
|
||||
float pri_box_offset_x = (pri_zone_col == 0) ? 0.0f : -imex_box_wx;
|
||||
float pri_box_offset_y = -imex_box_wy;
|
||||
@@ -1829,49 +1820,43 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
|
||||
int sec_zr = zone_row(sec_phys_row);
|
||||
const int sec_state = sec_tool_states[sec_tool_ids[i]];
|
||||
|
||||
// Y: all tools on a row share a Y rail — always zone-relative copy.
|
||||
float sec_zone_y = bed_y_min + (float)sec_zr * row_strip_height;
|
||||
float sec_y = sec_zone_y + rel_y;
|
||||
// A carriage stays inside its own zone; a Mirror reflects its
|
||||
// zone-relative offset about that zone's centerline, on the axis of the
|
||||
// boundary it shares with primary — the same rule the ghosts use (see
|
||||
// imex_head_transform / ImexMirrorAxis):
|
||||
// Copy → tracks primary on both axes.
|
||||
// Mirror, same gantry → reflect X (zones sit side by side).
|
||||
// Mirror, other gantry → reflect Y (zones sit front-to-back); X
|
||||
// tracks primary, since the part off that
|
||||
// gantry is a Y-reflection of the tool
|
||||
// directly behind it.
|
||||
const bool is_mirror = (sec_state == 3);
|
||||
const bool cross_gantry = (sec_phys_row != pri_phys_row);
|
||||
const float sec_zone_x = bed_x_min + (float)sec_zc * strip_width;
|
||||
const float sec_zone_y = bed_y_min + (float)sec_zr * row_strip_height;
|
||||
|
||||
// X: Copy → same zone-relative position.
|
||||
// Mirror → reflect copy reference across the boundary it shares with
|
||||
// this mirror zone (left or right edge of copy zone depending on side).
|
||||
const bool is_aggregated = sec_aggregated.count(sec_tool_ids[i]) > 0;
|
||||
float sec_x;
|
||||
if (sec_state == 2) {
|
||||
sec_x = bed_x_min + (float)sec_zc * strip_width + rel_x;
|
||||
} else if (is_aggregated) {
|
||||
// Aggregated Span gantry: the secondary strip spans full-X, so the
|
||||
// mirror axis is the bed centerline (no adjacent copy column to
|
||||
// reflect across). Reflecting across a zone edge here would push
|
||||
// the marker off the bed.
|
||||
sec_x = (bed_x_min + bed_x_max) - prim_pos.x();
|
||||
} else {
|
||||
auto ref_it = row_copy_col.find(sec_phys_row);
|
||||
int ref_phys_col = (ref_it != row_copy_col.end()) ? ref_it->second : pri_phys_col;
|
||||
int ref_zc = zone_col(ref_phys_col);
|
||||
float ref_zone_x = bed_x_min + (float)ref_zc * strip_width;
|
||||
float ref_abs = ref_zone_x + rel_x;
|
||||
if (sec_phys_col < ref_phys_col) {
|
||||
// Mirror left of copy — reflects across copy zone's left edge
|
||||
sec_x = 2.0f * ref_zone_x - ref_abs;
|
||||
} else {
|
||||
// Mirror right of copy — reflects across copy zone's right edge
|
||||
float ref_zone_right = bed_x_min + (float)(ref_zc + 1) * strip_width;
|
||||
sec_x = 2.0f * ref_zone_right - ref_abs;
|
||||
}
|
||||
}
|
||||
const float sec_x = (is_mirror && !cross_gantry)
|
||||
? sec_zone_x + (strip_width - rel_x)
|
||||
: sec_zone_x + rel_x;
|
||||
const float sec_y = (is_mirror && cross_gantry)
|
||||
? sec_zone_y + (row_strip_height - rel_y)
|
||||
: sec_zone_y + rel_y;
|
||||
|
||||
Vec3f sec_pos{ sec_x, sec_y, prim_pos.z() };
|
||||
m_sequential_view.m_imex_secondary_markers[i].set_world_position(sec_pos);
|
||||
m_sequential_view.m_imex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f);
|
||||
// Only an X-axis (same-gantry) mirror flips which side of the nozzle the
|
||||
// toolhead body sits on: reflecting the carriage in X reverses its
|
||||
// orientation. A cross-gantry mirror reflects in Y, so its X orientation
|
||||
// matches the tool directly behind it and the body stays on the same side
|
||||
// as primary's — same as a Copy.
|
||||
float sec_box_offset_x;
|
||||
if (sec_state == 2) {
|
||||
sec_box_offset_x = pri_box_offset_x;
|
||||
} else {
|
||||
if (is_mirror && !cross_gantry) {
|
||||
if (sec_phys_col > pri_phys_col) sec_box_offset_x = -imex_box_wx;
|
||||
else if (sec_phys_col < pri_phys_col) sec_box_offset_x = 0.0f;
|
||||
else sec_box_offset_x = pri_box_offset_x;
|
||||
} else {
|
||||
sec_box_offset_x = pri_box_offset_x;
|
||||
}
|
||||
float sec_box_offset_y;
|
||||
if (sec_phys_row > pri_phys_row) sec_box_offset_y = -imex_box_wy;
|
||||
|
||||
Reference in New Issue
Block a user