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 <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-05-01 01:27:47 -04:00
co-authored by Claude Opus 4.7
parent 4966d0fae8
commit 1d6fab7a44
+14 -3
View File
@@ -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<double, 3>(-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<double, 3>(-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);