From 1d6fab7a44ab3dddec03dcc83d7477e68058b7dd Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Fri, 1 May 2026 01:27:47 -0400 Subject: [PATCH] fix(imex): pivot Span ghost X-flip on mesh bbox center The aggregated-mirror ghost was X-flipping about the mesh's local origin, which shifted the ghost sideways for models whose local origin sits at a corner (calibration cubes, calicat, most STL imports anchored at the min corner). Visible as a constant left-X offset between the primary's position and the ghost's position. Pivot on `mo->raw_mesh_bounding_box().center()` instead, applied through the instance transform so rotated objects flip about the rotated bbox center too. Same correction applied to the live-drag update path. Co-Authored-By: Claude Opus 4.7 --- src/slic3r/GUI/PartPlate.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 743f8121d3..a81f7770eb 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1187,12 +1187,19 @@ void PartPlate::calc_imex_ghosts() // ghost X motion against primary serves no collision purpose and // makes the ghost drift off-bed when primary drags. Translate 1:1 // with primary (copy-style position) and bake the X-flip into the - // mesh-local frame so geometry still reads as mirrored. Equivalent - // to: ghost_xf = Translate(0, gantry_y, 0) * inst_world * X-flip. + // mesh-local frame so geometry still reads as mirrored. + // + // Flip pivots on the mesh's bbox center, not its local origin — + // models whose local origin sits at a corner (calibration cubes, + // STL imports anchored at min) would otherwise shift left by 2x + // the bbox-center offset. const Vec2d target_off = center_for(phys); + const Vec3d bc = mo->raw_mesh_bounding_box().center(); ghost_xf = inst_world; ghost_xf.linear() = ghost_xf.linear() * Eigen::DiagonalMatrix(-1.0, 1.0, 1.0); + ghost_xf.translation() += inst_world.linear() + * Vec3d(2.0 * bc.x(), 0.0, 0.0); ghost_xf.translation().y() += target_off.y() - primary_off.y(); } else { const auto [pri_center, gantry] = resolve_centers(phys); @@ -1309,11 +1316,15 @@ void PartPlate::update_imex_ghost_transforms( if (aggregated_mirror) { // Span aggregation: drop X reflection — gantries don't share an X rail // so reflecting motion serves no collision purpose. Translate 1:1 in X - // and bake X-flip into mesh-local frame so geometry still mirrors. + // and bake X-flip into mesh-local frame, pivoting on the bbox center so + // off-origin meshes don't shift sideways. Same math as calc_imex_ghosts. const Vec2d target_off = center_for(head); + const Vec3d bc = mo->raw_mesh_bounding_box().center(); ghost_xf = primary_xf; ghost_xf.linear() = ghost_xf.linear() * Eigen::DiagonalMatrix(-1.0, 1.0, 1.0); + ghost_xf.translation() += primary_xf.linear() + * Vec3d(2.0 * bc.x(), 0.0, 0.0); ghost_xf.translation().y() += target_off.y() - primary_off.y(); } else { const auto [pri_center, gantry] = resolve_centers(head);