Fix mirrored transforms when loading 3MF files (#15731)

This commit is contained in:
TheLegendTubaGuy
2026-10-04 12:53:34 -03:00
committed by GitHub
parent 000f8abc6d
commit 67a976e002
3 changed files with 82 additions and 8 deletions
+4 -4
View File
@@ -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)
+4 -4
View File
@@ -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)
+74
View File
@@ -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<Preset*> 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()