Fix scale and mirror of rotated mirrored objects (#16131)

* Fix decomposition of mirrored transformations

computeRotationScaling() may place the mirror on any axis, giving unstable rotation/scale for mirrored matrices. Decompose them by flipping each local axis and choosing the one needing the least rotation (lowest axis on ties). Merge the extract helpers into a single extract_rotation_scale() and simplify contains_skew() to check that the scale is diagonal.

set_scaling_factor() now preserves the mirror sign on the scaled axis. Selection::scale_and_translate() uses Transformation's rotation and scale instead of calling computeRotationScaling() directly, so scaling a mirrored instance adds no skew.

Add tests for mirrored decomposition, scaling, and skew detection.

* Use Eigen polar decomposition for instance scaling

Selection::scale_and_translate now splits the instance matrix with computeRotationScaling instead of get_rotation_matrix().

Adds a Model test checking that an instance created from another's offset, scale, rotation and mirror reproduces its matrix (as Add instance and Fill bed do).

* tidy
This commit is contained in:
Ian Bassi
2026-10-10 16:03:34 -03:00
committed by GitHub
parent e5d5fb47ca
commit ea10c84d6b
3 changed files with 176 additions and 41 deletions
+31 -41
View File
@@ -428,55 +428,44 @@ Transform3d Transformation::get_offset_matrix() const
return translation_transform(get_offset());
}
static Transform3d extract_rotation_matrix(const Transform3d& trafo)
{
Matrix3d rotation;
Matrix3d scale;
trafo.computeRotationScaling(&rotation, &scale);
return Transform3d(rotation);
}
static Transform3d extract_scale(const Transform3d& trafo)
{
Matrix3d rotation;
Matrix3d scale;
trafo.computeRotationScaling(&rotation, &scale);
return Transform3d(scale);
}
// computeRotationScaling() may put a mirror on any axis; use the local axis needing the least rotation.
static std::pair<Transform3d, Transform3d> extract_rotation_scale(const Transform3d& trafo)
{
Matrix3d rotation;
Matrix3d scale;
trafo.computeRotationScaling(&rotation, &scale);
if (trafo.linear().determinant() >= 0.0) {
trafo.computeRotationScaling(&rotation, &scale);
return { Transform3d(rotation), Transform3d(scale) };
}
for (int axis = 0; axis < 3; ++axis) {
Transform3d flipped = trafo;
flipped.linear().col(axis) *= -1.0;
Matrix3d r;
Matrix3d s;
flipped.computeRotationScaling(&r, &s);
// EPSILON keeps the lowest axis on ties, e.g. a mirror rotated by 90 degrees.
if (axis == 0 || r.trace() > rotation.trace() + EPSILON) {
rotation = r;
scale = s;
scale.col(axis) *= -1.0;
}
}
return { Transform3d(rotation), Transform3d(scale) };
}
static Transform3d extract_rotation_matrix(const Transform3d& trafo)
{
return extract_rotation_scale(trafo).first;
}
static Transform3d extract_scale(const Transform3d& trafo)
{
return extract_rotation_scale(trafo).second;
}
static bool contains_skew(const Transform3d& trafo)
{
Matrix3d rotation;
Matrix3d scale;
trafo.computeRotationScaling(&rotation, &scale);
if (scale.isDiagonal())
return false;
if (scale.determinant() >= 0.0)
return true;
// the matrix contains mirror
const Matrix3d ratio = scale.cwiseQuotient(trafo.matrix().block<3,3>(0,0));
auto check_skew = [&ratio](int i, int j, bool& skew) {
if (!std::isnan(ratio(i, j)) && !std::isnan(ratio(j, i)))
skew |= std::abs(ratio(i, j) * ratio(j, i) - 1.0) > EPSILON;
};
bool has_skew = false;
check_skew(0, 1, has_skew);
check_skew(0, 2, has_skew);
check_skew(1, 2, has_skew);
return has_skew;
return !extract_scale(trafo).linear().isDiagonal();
}
Vec3d Transformation::get_rotation() const
@@ -550,7 +539,8 @@ void Transformation::set_scaling_factor(Axis axis, double scaling_factor)
assert(scaling_factor > 0.0);
auto [rotation, scale] = extract_rotation_scale(m_matrix);
scale(axis, axis) = scaling_factor;
// Keep a mirror that sits on this axis.
scale(axis, axis) = std::copysign(scaling_factor, scale(axis, axis));
const Vec3d offset = get_offset();
m_matrix = rotation * scale;