From 67a976e0022230c30c1a5fe0bf4a5a54a300a6c9 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Sun, 4 Oct 2026 10:53:34 -0500 Subject: [PATCH] Fix mirrored transforms when loading 3MF files (#15731) --- src/libslic3r/Format/3mf.cpp | 8 ++-- src/libslic3r/Format/bbs_3mf.cpp | 8 ++-- tests/libslic3r/test_3mf.cpp | 74 ++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 2f457f634d..e6e7f322e3 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -1973,12 +1973,12 @@ ModelVolumeType type_from_string(const std::string &s) void _3MF_Importer::_apply_transform(ModelInstance& instance, const Transform3d& transform) { - Slic3r::Geometry::Transformation t(transform); - // invalid scale value, return - if (!t.get_scaling_factor().all()) + // Validate the affine matrix directly. Decomposing a valid mirrored transform to + // rotation and scale is not stable across Eigen versions and may yield a zero diagonal. + if (!transform.matrix().allFinite() || !transform.linear().fullPivLu().isInvertible()) return; - instance.set_transformation(t); + instance.set_transformation(Slic3r::Geometry::Transformation(transform)); } bool _3MF_Importer::_handle_start_config(const char** attributes, unsigned int num_attributes) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index fb4c877a04..09f9725109 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -4361,12 +4361,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) void _BBS_3MF_Importer::_apply_transform(ModelInstance& instance, const Transform3d& transform) { - Slic3r::Geometry::Transformation t(transform); - // invalid scale value, return - if (!t.get_scaling_factor().all()) + // Validate the affine matrix directly. Decomposing a valid mirrored transform to + // rotation and scale is not stable across Eigen versions and may yield a zero diagonal. + if (!transform.matrix().allFinite() || !transform.linear().fullPivLu().isInvertible()) return; - instance.set_transformation(t); + instance.set_transformation(Slic3r::Geometry::Transformation(transform)); } bool _BBS_3MF_Importer::_handle_start_config(const char** attributes, unsigned int num_attributes) diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index a8fd1eb888..ceb949285d 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -167,6 +167,80 @@ SCENARIO("Export+Import geometry to/from 3mf file cycle", "[3mf]") { } } +SCENARIO("Mirrored instance transforms survive 3mf round trips", "[3mf][Regression]") { + const bool bbs_format = GENERATE(false, true); + + GIVEN("an instance with a valid mirrored transform") { + Model model; + const std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; + REQUIRE(load_stl(src_file.c_str(), &model)); + model.add_default_instances(); + + Transform3d mirrored = Transform3d::Identity(); + mirrored.linear() << + 4.4408921e-16, 0.819152044, 0.573576436, + 1.0, -4.4408921e-16, -1.11022302e-16, + -5.55111512e-17, -0.573576436, 0.819152044; + mirrored.translation() = Vec3d(700.41477, -169.930939, 63.392571); + REQUIRE_THAT(mirrored.linear().determinant(), Catch::Matchers::WithinAbs(-1.0, 1e-8)); + model.objects.front()->instances.front()->set_transformation(Geometry::Transformation(mirrored)); + const TriangleMesh expected_mesh = model.mesh(); + + WHEN("the model is stored and loaded") { + ScopedTemporaryDir backup_dir("orca_mirrored_transform"); + model.set_backup_path(backup_dir.string()); + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + if (bbs_format) { + StoreParams store_params; + store_params.path = test_file.c_str(); + store_params.model = &model; + store_params.config = &config; + store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; + REQUIRE(store_bbs_3mf(store_params)); + } else { + REQUIRE(store_3mf(test_file.c_str(), &model, &config, false)); + } + + Model dst_model; + ScopedTemporaryDir loaded_backup_dir("orca_mirrored_transform_loaded"); + dst_model.set_backup_path(loaded_backup_dir.string()); + DynamicPrintConfig dst_config; + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + PlateDataPtrs dst_plates; + std::vector project_presets; + ScopeGuard cleanup([&dst_plates, &project_presets]() { + release_PlateData_list(dst_plates); + for (Preset *preset : project_presets) + delete preset; + }); + if (bbs_format) { + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + REQUIRE(load_bbs_3mf(test_file.c_str(), &dst_config, &ctxt, &dst_model, &dst_plates, + &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, + LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); + } else { + REQUIRE(load_3mf(test_file.c_str(), dst_config, ctxt, &dst_model, false)); + } + + THEN("the mirrored transform and world geometry are preserved") { + REQUIRE(dst_model.objects.size() == 1); + REQUIRE(dst_model.objects.front()->instances.size() == 1); + const Transform3d &loaded = dst_model.objects.front()->instances.front()->get_matrix(); + REQUIRE(loaded.linear().determinant() < 0.0); + + const TriangleMesh loaded_mesh = dst_model.mesh(); + REQUIRE(loaded_mesh.its.vertices.size() == expected_mesh.its.vertices.size()); + for (size_t i = 0; i < loaded_mesh.its.vertices.size(); ++i) + REQUIRE(loaded_mesh.its.vertices[i].isApprox(expected_mesh.its.vertices[i], 1e-5f)); + } + } + } +} + // The recipe is an opaque binary blob (CadDocument::serialize_recipe()), so the 3mf backend has // to carry it byte-for-byte — no XML/text mangling, embedded NULs intact. static std::string make_cad_recipe()