From 0bc8912667eee95e02f899199f61234c1ce7a8df Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Fri, 2 Oct 2026 17:59:51 +0800 Subject: [PATCH] Map BambuStudio Keys and Values to Their OrcaSlicer Equivalents BambuStudio projects lost or misread several settings on import: - Rename role_base_wipe_speed, no_slow_down_for_cooling_on_outwalls, process_notes, prime_tower_max_speed, enable_support_ironing and sparse_infill_lattice_angle_1/2 to their OrcaSlicer names. - Map the values 2dlattice, 2dhoneycomb and tree_organic. - Map BambuStudio's ensure_vertical_shell_thickness levels, and read top_one_wall_type = not apply as off. - Keep prime_tower_rib_wall = 0 as a rectangle wall instead of dropping the key, which fell back to a rib wall. - Read a negative raft_first_layer_expansion (BambuStudio's auto) as 2 mm, which also stops it failing the range check. - Rename whole entries of different_settings_to_system, so a rename no longer rewrites longer keys that contain the old name. --- src/libslic3r/PrintConfig.cpp | 72 ++++++++++++++++++++++-------- tests/libslic3r/test_config.cpp | 77 +++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 17 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d4d0c90272..7d29da8da2 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -10,6 +10,8 @@ #include "GCode/Thumbnails.hpp" #include #include +#include +#include #include #include #include @@ -9185,16 +9187,18 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "tree_support_wall_count" && value == "-1") { value = "0"; } else if (opt_key == "different_settings_to_system") { - std::string copy_value = value; - copy_value.erase(std::remove(copy_value.begin(), copy_value.end(), '\"'), copy_value.end()); // remove '"' in string - std::set split_keys = SplitStringAndRemoveDuplicateElement(copy_value, ";"); - for (std::string split_key : split_keys) { - std::string copy_key = split_key, copy_value = ""; - handle_legacy(copy_key, copy_value); - if (copy_key != split_key) { - ReplaceString(value, split_key, copy_key); - } + // Rename whole entries: a substring rename would also hit longer keys that contain the old name. + std::vector entries; + boost::split(entries, value, boost::is_any_of(";")); + for (std::string &entry : entries) { + std::string key = entry, unused_value; + key.erase(std::remove(key.begin(), key.end(), '\"'), key.end()); + std::string new_key = key; + handle_legacy(new_key, unused_value); + if (new_key != key) + ReplaceString(entry, key, new_key); } + value = boost::algorithm::join(entries, ";"); } else if (opt_key == "overhang_fan_threshold" && value == "5%") { value = "10%"; } else if( opt_key == "wall_infill_order" ) { @@ -9236,6 +9240,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va else if (value == "0"){ value = "ensure_moderate"; } + // BambuStudio's levels + else if (value == "enabled") + value = "ensure_all"; + else if (value == "partial") + value = "ensure_moderate"; + else if (value == "disabled") + value = "none"; } else if (opt_key == "rotate_solid_infill_direction") { opt_key = "solid_infill_rotate_template"; if (value == "1") { @@ -9251,9 +9262,11 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va opt_key = "chamber_temperature"; } else if (opt_key == "thumbnail_size") { opt_key = "thumbnails"; - } else if (opt_key == "top_one_wall_type" && value != "none") { + } else if (opt_key == "top_one_wall_type") { + // BambuStudio's "topmost" (one wall on the topmost layer only) widens to every top surface. opt_key = "only_one_wall_top"; - value = "1"; + if (!value.empty()) + value = (value == "not apply" || value == "none") ? "0" : "1"; } else if (opt_key == "initial_layer_flow_ratio") { opt_key = "bottom_solid_infill_flow_ratio"; } else if (opt_key == "ironing_direction") { @@ -9271,6 +9284,20 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va opt_key == "ironing_pattern" || opt_key == "support_ironing_pattern") && value == "zig-zag") { value = "rectilinear"; + } else if (opt_key == "sparse_infill_pattern" && value == "2dlattice") { + // BambuStudio's name, and OrcaSlicer's before the lateral patterns were renamed + value = "lateral-lattice"; + } else if (opt_key == "sparse_infill_pattern" && value == "2dhoneycomb") { + value = "lateral-honeycomb"; + } else if (opt_key == "sparse_infill_lattice_angle_1" || opt_key == "lattice_angle_1") { + opt_key = "lateral_lattice_angle_1"; + } else if (opt_key == "sparse_infill_lattice_angle_2" || opt_key == "lattice_angle_2") { + opt_key = "lateral_lattice_angle_2"; + } else if (opt_key == "support_style" && value == "tree_organic") { + value = "organic"; + } else if (opt_key == "raft_first_layer_expansion" && !value.empty() && value.front() == '-') { + // BambuStudio's auto: 2 mm for normal supports and rafts. Tree branches use tree_support_auto_brim here. + value = "2"; } else if (opt_key == "filament_map_mode") { if (value == "Auto") value = "Auto For Flush"; } @@ -9299,12 +9326,11 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } // Orca: Rename wipe tower ribs related options else if (opt_key == "prime_tower_rib_wall") { - if (value == "1") { - opt_key = "wipe_tower_wall_type"; - value = "rib"; - } else { - opt_key = ""; - } + opt_key = "wipe_tower_wall_type"; + if (value == "1" || value == "true") + value = "rib"; + else if (!value.empty()) + value = "rectangle"; } else if (opt_key == "prime_tower_extra_rib_length") { opt_key = "wipe_tower_extra_rib_length"; } else if (opt_key == "prime_tower_rib_width") { @@ -9316,6 +9342,18 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va } else if (opt_key == "machine_switch_extruder_time") { opt_key = "machine_tool_change_time"; } + // BambuStudio names of OrcaSlicer options + else if (opt_key == "role_base_wipe_speed") { + opt_key = "role_based_wipe_speed"; + } else if (opt_key == "no_slow_down_for_cooling_on_outwalls") { + opt_key = "dont_slow_down_outer_wall"; + } else if (opt_key == "process_notes") { + opt_key = "notes"; + } else if (opt_key == "prime_tower_max_speed") { + opt_key = "wipe_tower_max_purge_speed"; + } else if (opt_key == "enable_support_ironing") { + opt_key = "support_ironing"; + } else if (opt_key == "wall_direction" && value == "auto") { value = "ccw"; } diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 00c4d6d170..10a852207b 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -508,6 +508,83 @@ TEST_CASE("load_from_json hands a preset's include list to the caller instead of CHECK(config.opt_string("machine_end_gcode") == "M84"); } +TEST_CASE("handle_legacy maps BambuStudio keys and values to their OrcaSlicer equivalents", "[Config]") { + struct Row { std::string key, value, expected_key, expected_value; }; + const Row row = GENERATE(values({ + {"role_base_wipe_speed", "0", "role_based_wipe_speed", "0"}, + {"no_slow_down_for_cooling_on_outwalls", "1", "dont_slow_down_outer_wall", "1"}, + {"process_notes", "note", "notes", "note"}, + {"prime_tower_max_speed", "70", "wipe_tower_max_purge_speed", "70"}, + {"enable_support_ironing", "1", "support_ironing", "1"}, + {"sparse_infill_lattice_angle_1", "-30", "lateral_lattice_angle_1", "-30"}, + {"sparse_infill_lattice_angle_2", "20", "lateral_lattice_angle_2", "20"}, + {"lattice_angle_1", "-30", "lateral_lattice_angle_1", "-30"}, + {"lattice_angle_2", "20", "lateral_lattice_angle_2", "20"}, + {"sparse_infill_pattern", "2dlattice", "sparse_infill_pattern", "lateral-lattice"}, + {"sparse_infill_pattern", "2dhoneycomb", "sparse_infill_pattern", "lateral-honeycomb"}, + {"support_style", "tree_organic", "support_style", "organic"}, + {"raft_first_layer_expansion", "-1", "raft_first_layer_expansion", "2"}, + {"raft_first_layer_expansion", "3.5", "raft_first_layer_expansion", "3.5"}, + {"ensure_vertical_shell_thickness", "enabled", "ensure_vertical_shell_thickness", "ensure_all"}, + {"ensure_vertical_shell_thickness", "partial", "ensure_vertical_shell_thickness", "ensure_moderate"}, + {"ensure_vertical_shell_thickness", "disabled", "ensure_vertical_shell_thickness", "none"}, + {"top_one_wall_type", "not apply", "only_one_wall_top", "0"}, + {"top_one_wall_type", "all top", "only_one_wall_top", "1"}, + {"top_one_wall_type", "topmost", "only_one_wall_top", "1"}, + {"prime_tower_rib_wall", "1", "wipe_tower_wall_type", "rib"}, + {"prime_tower_rib_wall", "0", "wipe_tower_wall_type", "rectangle"}, + {"prime_tower_rib_wall", "true", "wipe_tower_wall_type", "rib"}, + // A rename touches whole entries, not keys that contain the old name. + {"different_settings_to_system", "lattice_angle_1;sparse_infill_lattice_angle_1;wall_loops", "different_settings_to_system", + "lateral_lattice_angle_1;lateral_lattice_angle_1;wall_loops"}, + // different_settings_to_system and JSON arrays pass the key alone + {"prime_tower_rib_wall", "", "wipe_tower_wall_type", ""}, + {"top_one_wall_type", "", "only_one_wall_top", ""}, + {"enable_height_slowdown", "1", "", "1"}, + {"counter_coef_2", "0.025", "", "0.025"}, + })); + t_config_option_key key = row.key; + std::string value = row.value; + PrintConfigDef::handle_legacy(key, value); + INFO(row.key << " = " << row.value); + CHECK(key == row.expected_key); + CHECK(value == row.expected_value); +} + +TEST_CASE("A BambuStudio project config loads its renamed settings without substitutions", "[Config]") { + ScopedTemporaryFile tmp(".json"); + { + boost::nowide::ofstream ofs(tmp.string()); + ofs << R"({"role_base_wipe_speed":"0","no_slow_down_for_cooling_on_outwalls":["1","0"],)" + R"("process_notes":"note","prime_tower_max_speed":"70","enable_support_ironing":"1",)" + R"("sparse_infill_pattern":"2dlattice","sparse_infill_lattice_angle_1":"-30",)" + R"("support_style":"tree_organic","ensure_vertical_shell_thickness":"partial",)" + R"("top_one_wall_type":"not apply","prime_tower_rib_wall":"0","raft_first_layer_expansion":"-1",)" + R"("enable_height_slowdown":["1"]})"; + } + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable); + std::map key_values; + std::string reason; + REQUIRE(config.load_from_json(tmp.string(), substitutions, true, key_values, reason) == 0); + CHECK(substitutions.substitutions.empty()); + CHECK(substitutions.unrecogized_keys == std::vector{"enable_height_slowdown"}); + + CHECK_FALSE(config.opt_bool("role_based_wipe_speed")); + CHECK(config.option("dont_slow_down_outer_wall")->values == std::vector{1, 0}); + CHECK(config.opt_string("notes") == "note"); + CHECK_THAT(config.opt_float("wipe_tower_max_purge_speed"), Catch::Matchers::WithinAbs(70., 1e-9)); + CHECK(config.opt_bool("support_ironing")); + CHECK(config.opt_enum("sparse_infill_pattern") == ipLateralLattice); + CHECK_THAT(config.opt_float("lateral_lattice_angle_1"), Catch::Matchers::WithinAbs(-30., 1e-9)); + CHECK(config.opt_enum("support_style") == smsTreeOrganic); + CHECK(config.opt_enum("ensure_vertical_shell_thickness") == evstModerate); + CHECK_FALSE(config.opt_bool("only_one_wall_top")); + CHECK(config.opt_enum("wipe_tower_wall_type") == wtwRectangle); + CHECK_THAT(config.opt_float("raft_first_layer_expansion"), Catch::Matchers::WithinAbs(2., 1e-9)); + CHECK(config.validate().count("raft_first_layer_expansion") == 0); +} + TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") { DynamicPrintConfig config; config.set_key_value("layer_height", new ConfigOptionFloat(0.2));