From 877fcdccf166580adcd036bb999896a5abd49bdc Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 22 May 2026 12:26:43 +0800 Subject: [PATCH 01/30] =?UTF-8?q?feat=EF=BC=9Aadd=20total=20length=20mm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/orca-slice-engine/JsonReport.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/orca-slice-engine/JsonReport.cpp b/src/orca-slice-engine/JsonReport.cpp index 1dea61532af..3800f0f1fdf 100644 --- a/src/orca-slice-engine/JsonReport.cpp +++ b/src/orca-slice-engine/JsonReport.cpp @@ -64,7 +64,8 @@ void output_slice_statistics(const SliceOutputStats& stats, // ---- Aggregate totals for print_info_total ---- double total_print_time = 0; - double total_weight = 0; + double total_weight = 0; + double total_length = 0; std::vector total_filaments; { // Deduplicate by (type, color), summing used_g across plates @@ -73,7 +74,8 @@ void output_slice_statistics(const SliceOutputStats& stats, for (const auto& plate : stats.plates) { if (plate.success) { total_print_time += plate.print_time; - total_weight += plate.total_filament_g; + total_weight += plate.total_filament_g; + total_length += plate.total_filament_m; for (const auto& detail : plate.filament_details) { auto it = std::find_if(agg.begin(), agg.end(), [&](const AggFilament& a) { return a.type == detail.type && a.color == detail.color; }); @@ -103,6 +105,7 @@ void output_slice_statistics(const SliceOutputStats& stats, print_info["print_time_seconds"] = round2(total_print_time); print_info["print_time_formatted"] = format_time_hhmmss(static_cast(total_print_time)); print_info["total_weight_g"] = round2(total_weight); + print_info["total_length_mm"] = round2(total_length); print_info["plate_count"] = static_cast(stats.plates.size()); print_info["filaments"] = total_filaments; root["print_info_total"] = std::move(print_info); From 561bd57afd1a35f23b48ed4520d5b29dbd383b84 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 22 May 2026 17:57:42 +0800 Subject: [PATCH 02/30] Fix estimated first layer printing time using wrong data source The gcode comment 'estimated first layer printing time' was incorrectly using machine.prepare_time (start custom gcode duration) instead of the actual first layer print time. This was masked in desktop builds where the full start gcode (~19s of homing/heating) coincidentally looked plausible, but was clearly broken in the cloud engine where start gcode is stripped for safety, leaving only ~1s. Fixed by computing the real first layer time as: layers_time[0] - prepare_time which matches the existing layer_duration calculation at line 1343. --- src/libslic3r/GCode/GCodeProcessor.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index a91f17e750d..f232ee5f3e6 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -4454,9 +4454,13 @@ void GCodeProcessor::run_post_process() PrintEstimatedStatistics::ETimeMode mode = static_cast(i); if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { char buf[128]; + // First layer actual print time = layer 0 total time - start gcode (prepare) time + float first_layer_time = 0.0f; + if (!machine.layers_time.empty()) + first_layer_time = std::max(0.0f, machine.layers_time[0] - machine.prepare_time); sprintf(buf, "; estimated first layer printing time (%s mode) = %s\n", (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", - get_time_dhms(machine.prepare_time).c_str()); + get_time_dhms(first_layer_time).c_str()); export_lines.append_line(buf); processed = true; } From 7ce1df2a8f79d3d78d9044c650e42167825d3c96 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 11:51:38 +0800 Subject: [PATCH 03/30] Fix wall speed stuck at defaults by applying full print config to GCode generator --- src/libslic3r/GCode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ff353d5e6b5..6dd4b40e323 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2212,6 +2212,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato m_enable_cooling_markers = true; this->apply_print_config(print.config()); + m_config.apply(print.full_print_config(), true); // m_volumetric_speed = DoExport::autospeed_volumetric_limit(print); print.throw_if_canceled(); From e345b7617fad97320a804f665ff31d2b184dfc6e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 12:12:24 +0800 Subject: [PATCH 04/30] Fix auto-brim overriding user-configured brim_width to zero --- src/libslic3r/Brim.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Brim.cpp b/src/libslic3r/Brim.cpp index e865efc7818..f9d82556cb2 100644 --- a/src/libslic3r/Brim.cpp +++ b/src/libslic3r/Brim.cpp @@ -964,7 +964,11 @@ static ExPolygons outer_inner_brim_area(const Print& print, // config brim width in auto-brim mode if (has_brim_auto) { double brimWidthRaw = configBrimWidthByVolumeGroups(adhesion, maxSpeed, groupVolumePtrs, volumeGroup.slices, groupHeight); - brim_width = scale_(floor(brimWidthRaw / flowWidth / 2) * flowWidth * 2); + double auto_brim = scale_(floor(brimWidthRaw / flowWidth / 2) * flowWidth * 2); + // Only use auto-calculated width if the algorithm determined brim is needed. + // Otherwise fall back to the user-configured brim_width. + if (auto_brim > 0) + brim_width = auto_brim; } for (const ExPolygon& ex_poly : volumeGroup.slices) { // BBS: additional brim width will be added if part's adhesion area is too small and brim is not generated From 25e3fa754ff8ce3478848c97e240d0fe77531c1d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 12:16:17 +0800 Subject: [PATCH 05/30] Fix first layer height deviation when model has Z offset in 3MF --- src/libslic3r/PrintApply.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index ae0fc36d17a..5ba1ff9bec8 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -149,9 +149,12 @@ static std::vector print_objects_from_model_object trafo.trafo = model_instance_transformation.get_matrix_with_applied_shrinkage_compensation(shrinkage_compensation); auto shift = Point::new_scale(trafo.trafo.data()[12], trafo.trafo.data()[13]); - // Reset the XY axes of the transformation. + // Reset the XYZ translation axes of the transformation. + // XY offsets are stored in shift for per-instance positioning; + // Z offset is discarded to ensure first layer starts at Z=0. trafo.trafo.data()[12] = 0; trafo.trafo.data()[13] = 0; + trafo.trafo.data()[14] = 0; // Search or insert a trafo. auto it = trafos.emplace(trafo).first; const_cast(*it).instances.emplace_back(PrintInstance{ nullptr, model_instance, shift }); From 3512fd45d2a8889fe2b25824e408fffc638f7506 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 14:08:47 +0800 Subject: [PATCH 06/30] Add exception boundary around process_plate to prevent crashes from propagating --- src/orca-slice-engine/SliceEngine.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 994a7fb62f0..c8fe1551b64 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -726,6 +726,7 @@ bool SliceEngine::validate_input() { // ============================================================================ void SliceEngine::process_plate(int plate_id) { + try { // --- Filter instances for this plate --- std::set identify_ids; int instances_on_plate = filter_instances(plate_id, identify_ids); @@ -865,6 +866,20 @@ void SliceEngine::process_plate(int plate_id) { // All retries exhausted BOOST_LOG_TRIVIAL(error) << "Slicing/export failed for plate " << (plate_id + 1) << " after " << MAX_RETRIES << " attempts"; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Unhandled exception processing plate " << (plate_id + 1) + << ": " << e.what(); + m_any_error = true; + set_error_type(EXIT_SLICING_ERROR); + m_stats.issues.push_back(make_error(plate_id, "INTERNAL_ERROR", + std::string("Plate ") + std::to_string(plate_id + 1) + " slicing failed: " + e.what())); + } catch (...) { + BOOST_LOG_TRIVIAL(error) << "Unhandled non-standard exception processing plate " << (plate_id + 1); + m_any_error = true; + set_error_type(EXIT_SLICING_ERROR); + m_stats.issues.push_back(make_error(plate_id, "INTERNAL_ERROR", + std::string("Plate ") + std::to_string(plate_id + 1) + " slicing failed with unknown error")); + } } // ============================================================================ From 64a294e7944ab105273843a0cf59bcffd5d970fe Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 14:12:32 +0800 Subject: [PATCH 07/30] Add centralized report_error() helper to prevent error-handling drift --- src/orca-slice-engine/SliceEngine.cpp | 9 +++++++++ src/orca-slice-engine/SliceEngine.hpp | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index c8fe1551b64..0b473f35507 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -1570,6 +1570,15 @@ void SliceEngine::package_output() { // Exit code derivation // ============================================================================ +void SliceEngine::report_error(int plate_id, int exit_code, const std::string& code, + const std::string& message, bool set_main_message) { + m_any_error = true; + set_error_type(exit_code); + m_stats.issues.push_back(make_error(plate_id, code, message)); + if (set_main_message && m_stats.error_message.empty()) + m_stats.error_message = message; +} + void SliceEngine::set_error_type(int code) { if (code > m_error_type) m_error_type = code; diff --git a/src/orca-slice-engine/SliceEngine.hpp b/src/orca-slice-engine/SliceEngine.hpp index 57eb46d2108..dad388cc0bf 100644 --- a/src/orca-slice-engine/SliceEngine.hpp +++ b/src/orca-slice-engine/SliceEngine.hpp @@ -93,6 +93,8 @@ class SliceEngine { void run_postprocessing(int plate_id, PlateSliceResult& result); // --- State --- + void report_error(int plate_id, int exit_code, const std::string& code, + const std::string& message, bool set_main_message = false); EngineConfig m_cfg; std::string m_output_path; SliceOutputStats m_stats; From 0c1516db8c2da11d456bc8f428347e07708d41b5 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 14:16:48 +0800 Subject: [PATCH 08/30] Add architecture documentation, named constants, and config flow comments to SliceEngine --- src/orca-slice-engine/SliceEngine.cpp | 4 ++-- src/orca-slice-engine/SliceEngine.hpp | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 0b473f35507..13fc4f4f194 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -741,8 +741,8 @@ void SliceEngine::process_plate(int plate_id) { // Calculate plate dimensions and origin (done before build-volume check // so the check can translate instances into plate-local coordinates). - double plate_width = 200.0; - double plate_depth = 200.0; + double plate_width = DEFAULT_PLATE_WIDTH; + double plate_depth = DEFAULT_PLATE_DEPTH; if (m_config.has("printable_area")) { auto printable_area_opt = m_config.option("printable_area"); diff --git a/src/orca-slice-engine/SliceEngine.hpp b/src/orca-slice-engine/SliceEngine.hpp index dad388cc0bf..432cc4a9de0 100644 --- a/src/orca-slice-engine/SliceEngine.hpp +++ b/src/orca-slice-engine/SliceEngine.hpp @@ -45,6 +45,30 @@ struct PlateSliceResult { std::vector issues; // collected issues for this plate }; +// ============================================================================ +// SliceEngine — headless cloud slicing pipeline +// ============================================================================ +// +// Pipeline stages: +// Stage 1 — load_3mf() → validate_config() → validate_presets() → validate_input() +// Stage 2 — per-plate: filter_instances() → build_volume_check() → apply_model() +// → validation() → slicing() → export_gcode() → postprocessing() +// Stage 3 — package_output() → build_statistics() +// +// Config flow (three layers, applied in order): +// 1. m_config — raw 3MF project_settings.config (DynamicPrintConfig) +// 2. merged_config — m_config + engine overrides (per-plate, G-code strip) +// (created in apply_model(), passed to Print::apply()) +// 3. m_full_print_config — Print's internal full config, includes all +// PrintConfig + PrintObjectConfig + PrintRegionConfig defaults +// (accessible via print.full_print_config()) +// +// Key design decisions: +// - Fresh Print object per retry attempt (no explicit dtor / placement-new) +// - Per-plate error handling via report_error() helper +// - try-catch boundary at process_plate() prevents one plate from crashing the job +// - Custom G-code stripped for cloud safety (apply_official_presets) +// class SliceEngine { public: SliceEngine(const EngineConfig& cfg, std::vector& temp_files); @@ -119,4 +143,7 @@ class SliceEngine { // Preset validation (requires system profiles at resources_dir/profiles/) std::unique_ptr m_preset_bundle; bool m_presets_available = false; + + static constexpr double DEFAULT_PLATE_WIDTH = 200.0; + static constexpr double DEFAULT_PLATE_DEPTH = 200.0; }; From 253ecdec1f151ccb2d3a0fdf017e93177ed77180 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 14:36:24 +0800 Subject: [PATCH 09/30] Fix pressure advance not emitted - gate on pressure_advance>0 instead of enable_pressure_advance bool --- src/libslic3r/GCode.cpp | 14 +++++++------- src/libslic3r/GCode/AdaptivePAProcessor.cpp | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 6dd4b40e323..81d9d6e3609 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -662,7 +662,7 @@ std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::T check_add_eol(toolchange_gcode_str); // SoftFever: set new PA for new filament - if (gcodegen.config().enable_pressure_advance.get_at(new_extruder_id)) { + if (gcodegen.config().pressure_advance.get_at(new_extruder_id) > 0) { gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_extruder_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value @@ -804,7 +804,7 @@ std::string WipeTowerIntegration::append_tcr2(GCode& gcodegen, const WipeTower:: check_add_eol(toolchange_gcode_str); // SoftFever: set new PA for new filament - if (new_extruder_id != -1 && gcodegen.config().enable_pressure_advance.get_at(new_extruder_id)) { + if (new_extruder_id != -1 && gcodegen.config().pressure_advance.get_at(new_extruder_id) > 0) { gcode += gcodegen.writer().set_pressure_advance(gcodegen.config().pressure_advance.get_at(new_extruder_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value @@ -7567,7 +7567,7 @@ std::string GCode::_extrude(const ExtrusionPath& path, std::string description, m_curr_print->calib_mode() == CalibMode::Calib_PA_Pattern || m_curr_print->calib_mode() == CalibMode::Calib_PA_Tower; bool evaluate_adaptive_pa = false; bool role_change = (m_last_extrusion_role != path.role()); - if (!is_pa_calib && EXTRUDER_CONFIG(adaptive_pressure_advance) && EXTRUDER_CONFIG(enable_pressure_advance)) { + if (!is_pa_calib && EXTRUDER_CONFIG(adaptive_pressure_advance) && EXTRUDER_CONFIG(pressure_advance) > 0) { evaluate_adaptive_pa = true; // If we have already emmited a PA change because the m_multi_flow_segment_path_pa_set is set // skip re-issuing the PA change tag. @@ -7739,7 +7739,7 @@ std::string GCode::_extrude(const ExtrusionPath& path, std::string description, // or a flow change, so emit the flag to evaluate PA for the upcomming extrusion // Emit tag before new speed is set so the post processor reads the next speed immediately and uses it. // Dont emit tag if it has just already been emitted from a role change above - if (_mm3_per_mm > 0 && EXTRUDER_CONFIG(adaptive_pressure_advance) && EXTRUDER_CONFIG(enable_pressure_advance) && + if (_mm3_per_mm > 0 && EXTRUDER_CONFIG(adaptive_pressure_advance) && EXTRUDER_CONFIG(pressure_advance) > 0 && EXTRUDER_CONFIG(adaptive_pressure_advance_overhangs) && !evaluate_adaptive_pa) { if (writer().get_current_speed() > F) { // Ramping down speed - use overhang logic where the minimum speed is used between current and upcoming extrusion @@ -7938,7 +7938,7 @@ std::string GCode::_extrude(const ExtrusionPath& path, std::string description, // ORCA: Adaptive PA code segment when adjusting PA within the same feature // There is a speed change or flow change so emit the flag to evaluate PA for the upcomming extrusion // Emit tag before new speed is set so the post processor reads the next speed immediately and uses it. - if (_mm3_per_mm > 0 && EXTRUDER_CONFIG(adaptive_pressure_advance) && EXTRUDER_CONFIG(enable_pressure_advance) && + if (_mm3_per_mm > 0 && EXTRUDER_CONFIG(adaptive_pressure_advance) && EXTRUDER_CONFIG(pressure_advance) > 0 && EXTRUDER_CONFIG(adaptive_pressure_advance_overhangs)) { if (last_set_speed > new_speed) { // Ramping down speed - use overhang logic where the minimum speed is used between // current and upcoming extrusion @@ -8437,7 +8437,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z, bool b gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, extruder_id, &config); check_add_eol(gcode); } - if (m_config.enable_pressure_advance.get_at(extruder_id)) { + if (m_config.pressure_advance.get_at(extruder_id) > 0) { gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(extruder_id)); // Orca: Adaptive PA // Reset Adaptive PA processor last PA value @@ -8652,7 +8652,7 @@ std::string GCode::set_extruder(unsigned int extruder_id, double print_z, bool b if (m_ooze_prevention.enable) gcode += m_ooze_prevention.post_toolchange(*this); - if (m_config.enable_pressure_advance.get_at(extruder_id)) { + if (m_config.pressure_advance.get_at(extruder_id) > 0) { gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(extruder_id)); } // Orca: tool changer or IDEX's firmware may change Z position, so we set it to unknown/undefined diff --git a/src/libslic3r/GCode/AdaptivePAProcessor.cpp b/src/libslic3r/GCode/AdaptivePAProcessor.cpp index 698c00cd139..c0642d1b24f 100644 --- a/src/libslic3r/GCode/AdaptivePAProcessor.cpp +++ b/src/libslic3r/GCode/AdaptivePAProcessor.cpp @@ -34,7 +34,7 @@ AdaptivePAProcessor::AdaptivePAProcessor(GCode &gcodegen, const std::vector 0){ auto interpolator = std::make_unique(); // Get calibration values from extruder std::string pa_calibration_values = m_config.adaptive_pressure_advance_model.get_at(tool); @@ -218,7 +218,7 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { if(!interpolator){ // Tool not found in the interpolator map // Tool not found in the PA interpolator to tool map - predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; + predicted_pa = m_config.pressure_advance.get_at(m_last_extruder_id) > 0 ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; if(m_config.gcode_comments) output << "; APA: Tool doesnt have APA enabled\n"; } else if (!interpolator->isInitialised() || (!m_config.adaptive_pressure_advance.get_at(m_last_extruder_id)) ) // Check if the model is not initialised by the constructor for the active extruder @@ -227,7 +227,7 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { // however check for robustness sake. { // Model failed or adaptive pressure advance not enabled - use default value from m_config - predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; + predicted_pa = m_config.pressure_advance.get_at(m_last_extruder_id) > 0 ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; if(m_config.gcode_comments) output << "; APA: Interpolator setup failed, using default pressure advance\n"; } else { // Model setup succeeded // Proceed to identify the print speed to use to calculate the adaptive PA value @@ -252,7 +252,7 @@ std::string AdaptivePAProcessor::process_layer(std::string &&gcode) { predicted_pa = m_config.adaptive_pressure_advance_bridges.get_at(m_last_extruder_id); if (predicted_pa < 0) { // If extrapolation fails, fall back to the default PA for the extruder. - predicted_pa = m_config.enable_pressure_advance.get_at(m_last_extruder_id) ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; + predicted_pa = m_config.pressure_advance.get_at(m_last_extruder_id) > 0 ? m_config.pressure_advance.get_at(m_last_extruder_id) : 0; if(m_config.gcode_comments) output << "; APA: Interpolation failed, using fallback pressure advance value\n"; } } From 333c293dc3b55cf28eb46ccb13fbda8faf792d4e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 15:56:20 +0800 Subject: [PATCH 10/30] Fix filament temperature loss for non-standard filaments using generic ConfigOptionVectorBase API --- src/orca-slice-engine/SliceEngine.cpp | 67 ++++++++++----------------- 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 13fc4f4f194..c49216b0345 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -577,55 +577,38 @@ void SliceEngine::substitute_filament_params(ConfigOptionStrings* filament_ids, const Preset& official_parent, const std::string& original_name) { - const std::string& parent_name = official_parent.name; + filament_ids->values[ext_idx] = official_parent.name; - filament_ids->values[ext_idx] = parent_name; - - const size_t ext_sz = static_cast(ext_idx); + const size_t dst_idx = static_cast(ext_idx); + // Iterate all config keys from the official parent preset. + // Use ConfigOption::is_nil() and set_at() to handle all types uniformly, + // including Nullable variants (nozzle_temperature, hot_plate_temp, etc.) + // that were previously skipped by explicit dynamic_cast branches. for (auto it = official_parent.config.cbegin(); it != official_parent.config.cend(); ++it) { - const auto& key = it->first; - const auto& opt = it->second; - auto* target = m_config.option(key, true); - if (!target) continue; - - size_t target_size = 0; - if (auto* dst = dynamic_cast(target)) { - target_size = dst->values.size(); - if (auto* src = dynamic_cast(opt.get())) { - if (!src->values.empty() && ext_sz < target_size) - dst->values[ext_idx] = src->values[0]; - } - } else if (auto* dst = dynamic_cast(target)) { - target_size = dst->values.size(); - if (auto* src = dynamic_cast(opt.get())) { - if (!src->values.empty() && ext_sz < target_size) - dst->values[ext_idx] = src->values[0]; - } - } else if (auto* dst = dynamic_cast(target)) { - target_size = dst->values.size(); - if (auto* src = dynamic_cast(opt.get())) { - if (!src->values.empty() && ext_sz < target_size) - dst->values[ext_idx] = src->values[0]; - } - } else if (auto* dst = dynamic_cast(target)) { - target_size = dst->values.size(); - if (auto* src = dynamic_cast(opt.get())) { - if (!src->values.empty() && ext_sz < target_size) - dst->values[ext_idx] = src->values[0]; - } - } else if (auto* dst = dynamic_cast(target)) { - target_size = dst->values.size(); - if (auto* src = dynamic_cast(opt.get())) { - if (!src->values.empty() && ext_sz < target_size) - dst->values[ext_idx] = src->values[0]; - } - } + const auto& key = it->first; + const auto& src_opt = it->second; + + auto* dst_opt = m_config.option(key, true); // get or create + if (!dst_opt) continue; + + // Only vector options can have per-extruder values; scalar options are shared. + auto* dst_vec = dynamic_cast(dst_opt); + if (!dst_vec) continue; + if (dst_vec->size() <= dst_idx) continue; + + // Only fill missing values — do not overwrite user-specified settings. + if (!dst_vec->is_nil(dst_idx)) continue; + + // Copy parent preset's first extruder value into the target extruder slot. + auto* src_vec = dynamic_cast(src_opt.get()); + if (src_vec && src_vec->size() > 0) + dst_vec->set_at(src_vec, dst_idx, 0); } m_stats.issues.push_back(make_warning(-1, "FILAMENT_SUBSTITUTED", std::string("Filament \"") + original_name - + "\" substituted with official preset \"" + parent_name + "\"")); + + "\" substituted with official preset \"" + official_parent.name + "\"")); } bool SliceEngine::validate_printer_model() From b364d1c2725706590d5f56c676c219c6a75c868c Mon Sep 17 00:00:00 2001 From: joyx-ubuntu Date: Wed, 27 May 2026 14:23:50 +0800 Subject: [PATCH 11/30] Fix crash when slicing 3MF files with missing filament_diameter and unsorted extruder retract keys - Sort m_extruder_retract_keys alphabetically so std::is_sorted assertion passes and binary search operations work correctly. Moved retract_length_toolchange and retract_restart_extra_toolchange to their proper positions. - Add null check for filament_diameter option in validate_presets() to prevent SIGSEGV when the config key is missing from 3MF files. --- src/libslic3r/PresetBundle.cpp | 3 ++- src/libslic3r/PrintConfig.cpp | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 8cefdafde74..005c3caed6e 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1096,7 +1096,8 @@ int PresetBundle::validate_presets(const std::string &file_name, DynamicPrintCon different_values = config.option("different_settings_to_system", true)->values; //PrinterTechnology printer_technology = Preset::printer_technology(config); - size_t filament_count = config.option("filament_diameter")->values.size(); + const auto* filament_diameter_opt = config.option("filament_diameter"); + size_t filament_count = filament_diameter_opt ? filament_diameter_opt->values.size() : 0; inherits_values.resize(filament_count + 2, std::string()); different_values.resize(filament_count + 2, std::string()); filament_preset_name.resize(filament_count, std::string()); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 3487801cf73..4168e2bfffe 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6430,10 +6430,12 @@ void PrintConfigDef::init_extruder_option_keys() "deretraction_speed", "long_retractions_when_cut", "retract_before_wipe", + "retract_length_toolchange", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retract_restart_extra", + "retract_restart_extra_toolchange", "retract_when_changing_layer", "retraction_distances_when_cut", "retraction_length", @@ -6444,9 +6446,7 @@ void PrintConfigDef::init_extruder_option_keys() "wipe_distance", "z_hop", "z_hop_types", - "z_hop_when_prime", - "retract_length_toolchange", - "retract_restart_extra_toolchange" + "z_hop_when_prime" }; assert(std::is_sorted(m_extruder_retract_keys.begin(), m_extruder_retract_keys.end())); } From 025e8838161f0c3283c03815fc34bd973ff37ffe Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 16:14:53 +0800 Subject: [PATCH 12/30] Reduce SliceEngine header dependencies with forward declarations --- src/orca-slice-engine/SliceEngine.cpp | 6 ++++++ src/orca-slice-engine/SliceEngine.hpp | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index c49216b0345..fff9fe52e5e 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -18,6 +18,10 @@ #include "libslic3r/GCode/PostProcessor.hpp" #include "libslic3r/ProjectTask.hpp" #include "libslic3r/BoundingBox.hpp" +#include "libslic3r/Format/bbs_3mf.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/Print.hpp" constexpr int MAX_RETRIES = 3; @@ -44,6 +48,8 @@ bool is_wipe_tower_error(const PlateSliceResult& result) { } // namespace +SliceEngine::~SliceEngine() = default; + SliceEngine::SliceEngine(const EngineConfig& cfg, std::vector& temp_files) : m_cfg(cfg) , m_temp_files(temp_files) diff --git a/src/orca-slice-engine/SliceEngine.hpp b/src/orca-slice-engine/SliceEngine.hpp index 432cc4a9de0..441727c6de1 100644 --- a/src/orca-slice-engine/SliceEngine.hpp +++ b/src/orca-slice-engine/SliceEngine.hpp @@ -8,14 +8,19 @@ #include "libslic3r/Config.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Model.hpp" -#include "libslic3r/Preset.hpp" -#include "libslic3r/PresetBundle.hpp" -#include "libslic3r/Print.hpp" -#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Semver.hpp" #include "Types.hpp" +// Forward declarations for types used by pointer/reference only +namespace Slic3r { + class Print; + class Preset; + class PresetBundle; + struct PlateData; +} +using PlateDataPtrs = std::vector; + struct EngineConfig { std::string input_file; std::string output_base; // -o value, empty = auto-derive from input name @@ -72,6 +77,7 @@ struct PlateSliceResult { class SliceEngine { public: SliceEngine(const EngineConfig& cfg, std::vector& temp_files); + ~SliceEngine(); // Run the full pipeline. Returns true if at least one plate produced output. bool run(); @@ -135,7 +141,7 @@ class SliceEngine { Slic3r::DynamicPrintConfig m_config; Slic3r::ConfigSubstitutionContext m_config_substitutions{ Slic3r::ForwardCompatibilitySubstitutionRule::Enable}; - Slic3r::PlateDataPtrs m_plate_data; + PlateDataPtrs m_plate_data; std::vector m_project_presets; bool m_is_bbl_3mf = false; Slic3r::Semver m_file_version; From 43a14ed31e3f4c83ffb12ea6ef6295fb5a6be438 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Wed, 27 May 2026 16:53:59 +0800 Subject: [PATCH 13/30] Block slicing on config validation errors to match desktop red-popup behavior --- src/orca-slice-engine/SliceEngine.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index fff9fe52e5e..77a6da4bc03 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -63,7 +63,7 @@ bool SliceEngine::run() { return false; } - // Config & preset validation (desktop parity — non-blocking) + // Config & preset validation (desktop parity) validate_config(); load_system_presets(); validate_presets(); @@ -87,6 +87,12 @@ bool SliceEngine::run() { return false; } + // Abort if earlier config validation detected unrecoverable invalid values + if (m_any_error) { + build_statistics(); + return false; + } + bool validate_ok = validate_input(); if (load_ok && validate_ok) { @@ -271,8 +277,12 @@ void SliceEngine::validate_config() { // A1: Validate config values (layer_height, nozzle_diameter, etc.) std::map invalid = m_config.validate(true); - for (const auto& [key, msg] : invalid) - m_stats.issues.push_back(make_error(-1, "CONFIG_INVALID_" + key, msg)); + if (!invalid.empty()) { + for (const auto& [key, msg] : invalid) + m_stats.issues.push_back(make_error(-1, "CONFIG_INVALID_" + key, msg)); + m_any_error = true; + set_error_type(EXIT_VALIDATION_ERROR); + } // A2: Check config substitutions (unknown keys, forward-compat changes) if (!m_config_substitutions.empty()) { From aa1e50dd241b0e8f9a2a4fa4195be2bf1f8d37b0 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 28 May 2026 16:34:27 +0800 Subject: [PATCH 14/30] =?UTF-8?q?Fix=20bed=20temperature=20always=20defaul?= =?UTF-8?q?ting=20to=2060=C2=B0C=20in=20cloud=20engine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge system printer + filament presets under the project config before passing to Print::apply(), mirroring the desktop PresetBundle::full_config() layered merge. Without this, filament-level keys (hot_plate_temp_initial_layer, nozzle_temperature, etc.) never reach the GCode generator. --- src/orca-slice-engine/SliceEngine.cpp | 75 ++++++++++++++++++++++++++- src/orca-slice-engine/SliceEngine.hpp | 15 +++--- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 77a6da4bc03..01bb42396b3 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -676,6 +676,77 @@ void SliceEngine::apply_official_presets() } +// ============================================================================ +// Build a full print config by merging system presets (printer + filament) +// underneath the project config from the 3MF. This mirrors what the desktop +// does via PresetBundle::full_config() / full_fff_config() and ensures that +// filament-level keys (nozzle_temperature, hot_plate_temp, etc.) carry the +// values from the matching system filament preset rather than the raw +// FullPrintConfig defaults. +// ============================================================================ + +DynamicPrintConfig SliceEngine::build_full_print_config() +{ + DynamicPrintConfig out; + out.apply(FullPrintConfig::defaults()); + + if (m_presets_available && m_preset_bundle) { + auto& bundle = *m_preset_bundle; + + // Layer 1: System printer config (Snapmaker U1) + auto* printer_id_opt = m_config.option("printer_settings_id"); + if (printer_id_opt && !printer_id_opt->value.empty()) { + const Preset* printer_preset = bundle.printers.find_preset(printer_id_opt->value, true); + if (printer_preset) + out.apply(printer_preset->config); + } + + // Layer 2: System filament config (per-extruder) + auto* filament_ids = m_config.option("filament_settings_id"); + if (filament_ids && !filament_ids->values.empty()) { + const size_t num_filaments = filament_ids->values.size(); + + // Collect filament config pointers for each extruder. + std::vector filament_configs; + for (size_t i = 0; i < num_filaments; ++i) { + const Preset* preset = bundle.filaments.find_preset(filament_ids->values[i], true); + if (preset) + filament_configs.push_back(&preset->config); + } + + if (!filament_configs.empty()) { + // Merge per-filament values into `out`, mirroring + // PresetBundle::full_fff_config() multi-filament logic. + for (const auto& key : filament_configs.front()->keys()) { + if (key == "compatible_prints" || key == "compatible_printers") + continue; + + ConfigOption* dst_opt = out.option(key, false); + if (!dst_opt) continue; + + if (dst_opt->is_scalar()) { + const ConfigOption* src = filament_configs.front()->option(key); + if (src) dst_opt->set(src); + } else { + auto* dst_vec = static_cast(dst_opt); + std::vector opts(num_filaments, nullptr); + for (size_t i = 0; i < num_filaments; ++i) + opts[i] = (i < filament_configs.size()) + ? filament_configs[i]->option(key) + : nullptr; + dst_vec->set(opts); + } + } + } + } + } + + // Layer 3: Project config from 3MF (highest priority) + out.apply(m_config); + + return out; +} + // ============================================================================ // Stage 3: Validate input // ============================================================================ @@ -1069,7 +1140,9 @@ bool SliceEngine::apply_model(int plate_id, Print& print, const Vec3d& origin) { // // Work on a per-plate copy so extruder-count trimming does not leak // into subsequent plates (m_config is shared across the pipeline). - DynamicPrintConfig merged_config = m_config; + // Build full config (system defaults + printer + filament + project) + // before per-plate trimming and overrides. + DynamicPrintConfig merged_config = build_full_print_config(); { std::set used_extruders; for (ModelObject* obj : m_model.objects) { diff --git a/src/orca-slice-engine/SliceEngine.hpp b/src/orca-slice-engine/SliceEngine.hpp index 441727c6de1..3654fe76c99 100644 --- a/src/orca-slice-engine/SliceEngine.hpp +++ b/src/orca-slice-engine/SliceEngine.hpp @@ -60,13 +60,13 @@ struct PlateSliceResult { // → validation() → slicing() → export_gcode() → postprocessing() // Stage 3 — package_output() → build_statistics() // -// Config flow (three layers, applied in order): -// 1. m_config — raw 3MF project_settings.config (DynamicPrintConfig) -// 2. merged_config — m_config + engine overrides (per-plate, G-code strip) -// (created in apply_model(), passed to Print::apply()) -// 3. m_full_print_config — Print's internal full config, includes all -// PrintConfig + PrintObjectConfig + PrintRegionConfig defaults -// (accessible via print.full_print_config()) +// Config flow (four layers, applied in order): +// 1. FullPrintConfig::defaults() — system defaults +// 2. System printer preset — Snapmaker U1, from m_preset_bundle +// 3. System filament preset — per-extruder, from m_preset_bundle +// 4. m_config — 3MF project config (highest priority) +// Merged in build_full_print_config(), then engine overrides + per-plate +// applied in apply_model() before passing to Print::apply(). // // Key design decisions: // - Fresh Print object per retry attempt (no explicit dtor / placement-new) @@ -102,6 +102,7 @@ class SliceEngine { void load_system_presets(); void validate_presets(); void apply_official_presets(); + Slic3r::DynamicPrintConfig build_full_print_config(); bool validate_filament_official(); void substitute_filament_params(Slic3r::ConfigOptionStrings* filament_ids, int ext_idx, const Slic3r::Preset& official_parent, From 8eed0a3229a5ec4d4a3841a4f3247a130b3a61c6 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Thu, 28 May 2026 16:58:56 +0800 Subject: [PATCH 15/30] Replace printer G-code blocks with official U1 preset values Instead of clearing all G-code to empty strings, walk the inherits chain to find the official Snapmaker printer preset and source its default G-code values (machine_start_gcode, change_filament_gcode, etc.). Falls back to nozzle-diameter-matched U1 preset when no official ancestor is found. Filament/print-level G-code keys remain cleared for cloud safety. --keep-custom-gcode semantics unchanged. --- src/orca-slice-engine/SliceEngine.cpp | 94 +++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 01bb42396b3..cfee1a2bebf 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -658,22 +658,90 @@ bool SliceEngine::validate_printer_model() void SliceEngine::apply_official_presets() { - // Strip all custom G-code blocks — cloud slicing must not execute - // or embed user-supplied G-code for safety and consistency. - constexpr const char* gcode_keys[] = { - "start_gcode", "end_gcode", "layer_gcode", - "machine_start_gcode", "machine_end_gcode", - "before_layer_change_gcode", "between_objects_gcode", - "toolchange_gcode", "print_host", - }; - for (const char* key : gcode_keys) { - if (m_config.has(key)) { - m_config.set_key_value(key, new ConfigOptionString("")); - m_stats.issues.push_back(make_tip(-1, "GCODE_CLEARED", - std::string("Custom G-code '") + key + "' cleared for cloud safety")); + // Find the official Snapmaker printer preset to source default G-code blocks. + // Mirrors the filament substitution logic: walk the inherits chain upward, + // fall back to nozzle-diameter-matched U1 preset if no official ancestor found. + const Preset* official_printer = nullptr; + + if (m_presets_available && m_preset_bundle) { + auto& bundle = *m_preset_bundle; + + // Check whether a system preset is "official" (Snapmaker vendor) + auto is_official = [](const Preset& p) -> bool { + return p.vendor && p.vendor->name == PresetBundle::SM_BUNDLE; + }; + + auto* printer_id = m_config.option("printer_settings_id"); + std::string printer_name = printer_id ? printer_id->value : ""; + + // Look up the printer preset — system first, then project-embedded + if (!printer_name.empty()) { + const Preset* sys = bundle.printers.find_preset(printer_name, false); + const Preset* current = (sys && sys->name == printer_name) ? sys : nullptr; + + // Walk inherits chain to find official Snapmaker ancestor + std::set visited; + while (current) { + if (is_official(*current)) { + official_printer = current; + break; + } + std::string parent_name = current->inherits(); + if (parent_name.empty() || !visited.insert(parent_name).second) + break; + current = bundle.printers.find_preset(parent_name, false); + } + } + + // Fallback: match by nozzle diameter + if (!official_printer) { + std::string nozzle_str = "0.4"; + auto* nd = m_config.option("nozzle_diameter"); + if (nd && !nd->values.empty()) { + double dia = nd->values[0]; + if (dia < 0.3) nozzle_str = "0.2"; + else if (dia < 0.5) nozzle_str = "0.4"; + else if (dia < 0.7) nozzle_str = "0.6"; + else nozzle_str = "0.8"; + } + std::string fallback = "Snapmaker U1 (" + nozzle_str + " nozzle)"; + official_printer = bundle.printers.find_preset(fallback, true); + } + } + + // Replace every G-code key the official printer preset provides. + // Any other G-code-related key still gets cleared for cloud safety. + std::set replaced; + if (official_printer) { + for (auto it = official_printer->config.cbegin(); + it != official_printer->config.cend(); ++it) { + const std::string& key = it->first; + if (key.find("gcode") == std::string::npos && key != "print_host") + continue; + if (!m_config.has(key)) continue; + + std::string value; + auto* opt = official_printer->config.option(key); + if (opt) value = opt->value; + m_config.set_key_value(key, new ConfigOptionString(value)); + replaced.insert(key); + m_stats.issues.push_back(make_tip(-1, "GCODE_REPLACED", + std::string("G-code '") + key + "' replaced with official default")); } } + // Clear any remaining G-code keys not covered by the official printer preset + constexpr const char* known_gcode_keys[] = { + "start_gcode", "end_gcode", "layer_gcode", + "between_objects_gcode", "toolchange_gcode", + }; + for (const char* key : known_gcode_keys) { + if (!m_config.has(key)) continue; + if (replaced.count(key)) continue; + m_config.set_key_value(key, new ConfigOptionString("")); + m_stats.issues.push_back(make_tip(-1, "GCODE_CLEARED", + std::string("Custom G-code '") + key + "' cleared for cloud safety")); + } } // ============================================================================ From 11b558d525cff5bdde087de870a9492ac8dbb2e1 Mon Sep 17 00:00:00 2001 From: joyx-ubuntu Date: Thu, 28 May 2026 18:05:08 +0800 Subject: [PATCH 16/30] fix(slice-engine): preserve JSON filename on early exit and support custom presets - Compute m_output_path before any early-return path so JSON is always written to the correct filename (was ".json" when slicing failed early). - Apply FullPrintConfig::defaults() as baseline before 3MF loading to prevent Print::apply() crashes on missing config options. This mirrors PresetBundle::full_fff_config() in the desktop pipeline. - Add apply_printer_preset_config() to merge the printer preset config from system or project-embedded presets into m_config. - Refactor validate_filament_official(bool enforce): when enforce=false (--allow-custom-presets), non-official filaments produce warnings instead of errors. Add has_inline_filament_config() fallback for 3MF files where filament config values are embedded directly in project_settings.config without a named preset definition. - Always run filament validation; the enforce flag controls severity rather than whether validation runs at all. --- src/orca-slice-engine/SliceEngine.cpp | 166 +++++++++++++++++++++++--- src/orca-slice-engine/SliceEngine.hpp | 4 +- 2 files changed, 153 insertions(+), 17 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index cfee1a2bebf..294ae2d15fe 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -22,6 +22,7 @@ #include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/PrintConfig.hpp" constexpr int MAX_RETRIES = 3; @@ -57,6 +58,17 @@ SliceEngine::SliceEngine(const EngineConfig& cfg, std::vector& temp } bool SliceEngine::run() { + // Pre-compute output path so JSON is written to the correct filename + // even when the slicing pipeline returns early (e.g., filament validation). + m_output_path = generate_output_path(m_cfg.input_file, m_cfg.output_base, + m_cfg.plate_id, m_cfg.format, m_cfg.single_plate); + + // Apply FullPrintConfig defaults as baseline so every config key has + // a valid value before the 3MF overlays project settings on top. + // This mirrors PresetBundle::full_fff_config() and prevents + // Print::apply() crashes on missing options. + m_config.apply(FullPrintConfig::defaults()); + bool load_ok = load_3mf(); if (!load_ok) { build_statistics(); @@ -67,13 +79,15 @@ bool SliceEngine::run() { validate_config(); load_system_presets(); validate_presets(); + apply_printer_preset_config(); - // Filament official compliance check & substitution - if (m_cfg.substitute_filaments) { - if (!validate_filament_official()) { - build_statistics(); - return false; - } + // Filament validation: always run to catch truly invalid presets. + // When substitute_filaments is true (default), enforce official-only policy. + // When false (--allow-custom-presets), validate structural soundness but + // accept custom filaments with a warning. + if (!validate_filament_official(m_cfg.substitute_filaments)) { + build_statistics(); + return false; } // Strip custom G-code blocks for cloud safety @@ -123,9 +137,6 @@ bool SliceEngine::run() { } } - m_output_path = generate_output_path(m_cfg.input_file, m_cfg.output_base, - m_cfg.plate_id, m_cfg.format, m_cfg.single_plate); - // Collect plates to process (internal plate_index is 0-based) std::vector plates_to_process; if (m_cfg.single_plate) { @@ -462,7 +473,76 @@ void SliceEngine::validate_presets() } } -bool SliceEngine::validate_filament_official() +void SliceEngine::apply_printer_preset_config() +{ + // Try to merge the printer preset config (system or project-embedded) + // into m_config to get machine-specific values (printable_area, + // printable_height, etc.). + // + // FullPrintConfig defaults are applied separately before load_3mf() + // to provide a baseline for all config keys. + if (!m_presets_available || !m_preset_bundle) + return; + if (!m_config.has("printer_settings_id")) + return; + + const std::string printer_name = m_config.opt_string("printer_settings_id"); + if (printer_name.empty()) + return; + + // Look up the printer preset: try system first, then project-embedded. + const Preset* printer_preset = m_preset_bundle->printers.find_preset(printer_name, false); + if (!printer_preset || printer_preset->name != printer_name) { + for (auto* pp : m_project_presets) { + if (pp && pp->name == printer_name && pp->type == Preset::TYPE_PRINTER) { + printer_preset = pp; + break; + } + } + } + + if (!printer_preset) { + BOOST_LOG_TRIVIAL(info) << "Printer preset '" << printer_name + << "' not found; using defaults only"; + return; + } + + BOOST_LOG_TRIVIAL(info) << "Applying printer preset config: " << printer_name; + m_config.apply(printer_preset->config); +} + +bool SliceEngine::has_inline_filament_config(int ext_idx) +{ + // Check whether the config has per-extruder filament parameters for + // the given extruder index, even when no named filament preset exists. + // This handles 3MF files where filament config values are embedded + // directly in project_settings.config without a preset definition. + auto is_non_nil = [&](const char* key) -> bool { + if (!m_config.has(key)) return false; + auto* opt = m_config.option(key, true); + if (!opt) return false; + if (static_cast(opt->values.size()) <= ext_idx) return false; + return !opt->is_nil(ext_idx) && opt->values[ext_idx] > 0; + }; + + // nozzle_temperature or filament_diameter is a strong signal that + // filament config is present inline. + if (is_non_nil("nozzle_temperature")) return true; + if (is_non_nil("filament_diameter")) return true; + + // Fallback: check if filament_type is set (weaker signal, but + // confirms the extruder has filament assigned). + if (m_config.has("filament_type")) { + auto* ft = m_config.option("filament_type", true); + if (ft && ext_idx < static_cast(ft->values.size()) + && !ft->values[ext_idx].empty()) + return true; + } + + return false; +} + +bool SliceEngine::validate_filament_official(bool enforce) { // Skip if system presets are not available (no reference for comparison) if (!m_presets_available || !m_preset_bundle) @@ -478,6 +558,20 @@ bool SliceEngine::validate_filament_official() int num_filaments = static_cast(filament_ids->values.size()); bool any_error = false; + // Helper: report an issue at the appropriate severity. + // When not enforcing, non-official filaments are warnings instead of errors. + // Truly invalid presets (missing, broken inheritance) remain errors either way. + auto report = [&](bool is_official_violation, const std::string& code, const std::string& msg) { + if (!enforce && is_official_violation) { + BOOST_LOG_TRIVIAL(warning) << msg; + m_stats.issues.push_back(make_warning(-1, code, msg)); + } else { + BOOST_LOG_TRIVIAL(error) << msg; + m_stats.issues.push_back(make_error(-1, code, msg)); + any_error = true; + } + }; + // Lambda: check whether a system preset is "official" (Snapmaker or OrcaFilamentLibrary) auto is_official_preset = [](const Preset& p) -> bool { if (p.vendor && p.vendor->name == PresetBundle::SM_BUNDLE) @@ -511,22 +605,62 @@ bool SliceEngine::validate_filament_official() continue; // OK } std::string msg = "Filament \"" + name + "\" belongs to unsupported vendor"; - BOOST_LOG_TRIVIAL(error) << msg; - m_stats.issues.push_back(make_error(-1, "FILAMENT_UNSUPPORTED_VENDOR", msg)); - any_error = true; + report(/*is_official_violation=*/true, "FILAMENT_UNSUPPORTED_VENDOR", msg); continue; } // Case 2: Not a direct system match — walk the inheritance chain Preset* current = find_in_project(name); if (!current) { + // When not enforcing, filament config values may be embedded + // directly in project_settings.config without a named preset. + if (!enforce && has_inline_filament_config(i)) { + std::string msg = "Filament \"" + name + + "\" is an inline custom filament (no preset definition, " + + "but per-extruder config values are present)"; + BOOST_LOG_TRIVIAL(warning) << msg; + m_stats.issues.push_back(make_warning(-1, "FILAMENT_CUSTOM_INLINE", msg)); + continue; + } std::string msg = "Filament \"" + name + "\" is not a recognized preset"; - BOOST_LOG_TRIVIAL(error) << msg; - m_stats.issues.push_back(make_error(-1, "FILAMENT_UNKNOWN", msg)); - any_error = true; + // FILAMENT_UNKNOWN: preset truly doesn't exist — always an error + report(/*is_official_violation=*/false, "FILAMENT_UNKNOWN", msg); + continue; + } + + // If not enforcing and the preset exists in project, validate + // structural soundness (inheritance chain) but don't require + // an official ancestor. + if (!enforce) { + // Walk the chain just far enough to detect circular/invalid + // inheritance — accept the preset regardless of ancestry. + std::set visited; + Preset* walk = current; + bool chain_ok = true; + while (walk) { + std::string parent = walk->inherits(); + if (parent.empty()) break; // root reached, acceptable + if (!visited.insert(parent).second) { + std::string msg = "Circular inheritance detected in filament \"" + name + "\""; + BOOST_LOG_TRIVIAL(error) << msg; + m_stats.issues.push_back(make_error(-1, "FILAMENT_CIRCULAR_INHERITS", msg)); + any_error = true; + chain_ok = false; + break; + } + Preset* next = find_in_system(parent); + if (!next) next = find_in_project(parent); + walk = next; + } + if (!chain_ok) continue; + // Custom filament with sound structure — accepted with warning + std::string msg = "Filament \"" + name + "\" is a custom preset (not official)"; + BOOST_LOG_TRIVIAL(warning) << msg; + m_stats.issues.push_back(make_warning(-1, "FILAMENT_CUSTOM", msg)); continue; } + // Enforce mode: must resolve to an official ancestor bool resolved = false; std::set visited; while (current && !resolved) { diff --git a/src/orca-slice-engine/SliceEngine.hpp b/src/orca-slice-engine/SliceEngine.hpp index 3654fe76c99..6458d325b44 100644 --- a/src/orca-slice-engine/SliceEngine.hpp +++ b/src/orca-slice-engine/SliceEngine.hpp @@ -103,7 +103,9 @@ class SliceEngine { void validate_presets(); void apply_official_presets(); Slic3r::DynamicPrintConfig build_full_print_config(); - bool validate_filament_official(); + bool validate_filament_official(bool enforce = true); + bool has_inline_filament_config(int ext_idx); + void apply_printer_preset_config(); void substitute_filament_params(Slic3r::ConfigOptionStrings* filament_ids, int ext_idx, const Slic3r::Preset& official_parent, const std::string& original_name); From 5d1c6a3203247a6645a229f41c1a2d6fadd643e3 Mon Sep 17 00:00:00 2001 From: joyx-ubuntu Date: Thu, 28 May 2026 18:31:58 +0800 Subject: [PATCH 17/30] refactor(slice-engine): clean up code smells in preset config handling - apply_printer_preset_config: iterate config keys and only fill nil values instead of blindly applying the whole preset config, so project config values are not overwritten. - validate_filament_official: extract find_ancestor and walk_chain lambdas to DRY up parent lookup and circular-inheritance detection shared between enforce and non-enforce paths. Non-enforce path now also reports FILAMENT_UNKNOWN_ANCESTOR instead of silently accepting it. --- src/orca-slice-engine/SliceEngine.cpp | 133 +++++++++++++++++--------- 1 file changed, 86 insertions(+), 47 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 294ae2d15fe..d9f0a66d4ae 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -508,7 +508,30 @@ void SliceEngine::apply_printer_preset_config() } BOOST_LOG_TRIVIAL(info) << "Applying printer preset config: " << printer_name; - m_config.apply(printer_preset->config); + // Only fill keys that are missing (nil) in m_config — do not overwrite + // values already set by the project config or FullPrintConfig defaults. + // This matches the semantics of substitute_filament_params(). + for (auto it = printer_preset->config.cbegin(); + it != printer_preset->config.cend(); ++it) { + const auto& key = it->first; + auto* dst_opt = m_config.option(key, false); + if (!dst_opt) continue; + // Scalar options: only copy if nil. + if (dst_opt->is_scalar()) { + if (!dst_opt->is_nil()) continue; + dst_opt->set(it->second.get()); + } else { + // Vector options: copy per-element only if nil at that index, + // matching substitute_filament_params() per-extruder semantics. + auto* dst_vec = dynamic_cast(dst_opt); + auto* src_vec = dynamic_cast(it->second.get()); + if (!dst_vec || !src_vec) continue; + for (size_t i = 0; i < dst_vec->size() && i < src_vec->size(); ++i) { + if (dst_vec->is_nil(i)) + dst_vec->set_at(src_vec, i, i); + } + } + } } bool SliceEngine::has_inline_filament_config(int ext_idx) @@ -628,31 +651,45 @@ bool SliceEngine::validate_filament_official(bool enforce) continue; } - // If not enforcing and the preset exists in project, validate - // structural soundness (inheritance chain) but don't require - // an official ancestor. - if (!enforce) { - // Walk the chain just far enough to detect circular/invalid - // inheritance — accept the preset regardless of ancestry. - std::set visited; - Preset* walk = current; - bool chain_ok = true; + // Common: look up a parent by name in system or project presets. + auto find_ancestor = [&](const std::string& inherits_name) -> Preset* { + if (Preset* p = find_in_system(inherits_name)) return p; + return find_in_project(inherits_name); + }; + + // Common walk: follow the inheritance chain, detecting circular refs + // and unknown ancestors. 'walk' is advanced through the chain; returns + // false if a structural error is found (circular / unknown ancestor). + auto walk_chain = [&](Preset*& walk, std::set& visited) -> bool { while (walk) { - std::string parent = walk->inherits(); - if (parent.empty()) break; // root reached, acceptable - if (!visited.insert(parent).second) { + std::string inherits_name = walk->inherits(); + if (inherits_name.empty()) return true; // root reached + if (!visited.insert(inherits_name).second) { std::string msg = "Circular inheritance detected in filament \"" + name + "\""; BOOST_LOG_TRIVIAL(error) << msg; m_stats.issues.push_back(make_error(-1, "FILAMENT_CIRCULAR_INHERITS", msg)); any_error = true; - chain_ok = false; - break; + return false; + } + Preset* next = find_ancestor(inherits_name); + if (!next) { + std::string msg = "Filament \"" + name + "\" inherits from unknown preset \"" + + inherits_name + "\""; + BOOST_LOG_TRIVIAL(error) << msg; + m_stats.issues.push_back(make_error(-1, "FILAMENT_UNKNOWN_ANCESTOR", msg)); + any_error = true; + return false; } - Preset* next = find_in_system(parent); - if (!next) next = find_in_project(parent); walk = next; } - if (!chain_ok) continue; + return true; // empty inherits + }; + + // Non-enforce mode: validate structural soundness, accept regardless of ancestry. + if (!enforce) { + Preset* walk = current; + std::set visited; + if (!walk_chain(walk, visited)) continue; // Custom filament with sound structure — accepted with warning std::string msg = "Filament \"" + name + "\" is a custom preset (not official)"; BOOST_LOG_TRIVIAL(warning) << msg; @@ -660,7 +697,7 @@ bool SliceEngine::validate_filament_official(bool enforce) continue; } - // Enforce mode: must resolve to an official ancestor + // Enforce mode: must resolve to an official ancestor. bool resolved = false; std::set visited; while (current && !resolved) { @@ -671,7 +708,6 @@ bool SliceEngine::validate_filament_official(bool enforce) BOOST_LOG_TRIVIAL(error) << msg; m_stats.issues.push_back(make_error(-1, "FILAMENT_NO_OFFICIAL_ANCESTOR", msg)); any_error = true; - resolved = true; break; } @@ -680,37 +716,40 @@ bool SliceEngine::validate_filament_official(bool enforce) BOOST_LOG_TRIVIAL(error) << msg; m_stats.issues.push_back(make_error(-1, "FILAMENT_CIRCULAR_INHERITS", msg)); any_error = true; - resolved = true; break; } - // Try system presets first - if (Preset* parent = find_in_system(inherits_name)) { - if (is_official_preset(*parent)) { - substitute_filament_params(filament_ids, i, *parent, name); - resolved = true; - } else { - std::string vendor_name = parent->vendor ? parent->vendor->name : "unknown"; - std::string msg = "Filament \"" + name + "\" derives from unsupported vendor \"" - + vendor_name + "\" via \"" + inherits_name + "\""; - BOOST_LOG_TRIVIAL(error) << msg; - m_stats.issues.push_back(make_error(-1, "FILAMENT_UNSUPPORTED_VENDOR", msg)); - any_error = true; - resolved = true; - } + Preset* parent = find_ancestor(inherits_name); + if (!parent) { + std::string msg = "Filament \"" + name + "\" inherits from unknown preset \"" + + inherits_name + "\""; + BOOST_LOG_TRIVIAL(error) << msg; + m_stats.issues.push_back(make_error(-1, "FILAMENT_UNKNOWN_ANCESTOR", msg)); + any_error = true; + break; + } + + if (is_official_preset(*parent)) { + substitute_filament_params(filament_ids, i, *parent, name); + resolved = true; + } else if (parent->vendor) { + std::string vendor_name = parent->vendor->name; + std::string msg = "Filament \"" + name + "\" derives from unsupported vendor \"" + + vendor_name + "\" via \"" + inherits_name + "\""; + BOOST_LOG_TRIVIAL(error) << msg; + m_stats.issues.push_back(make_error(-1, "FILAMENT_UNSUPPORTED_VENDOR", msg)); + any_error = true; + resolved = true; + } else if (parent->type == Preset::TYPE_FILAMENT) { + // Project-embedded filament — continue walking up + current = parent; } else { - // Not in system — try project embedded, then continue walking - Preset* project_parent = find_in_project(inherits_name); - if (project_parent) { - current = project_parent; - } else { - std::string msg = "Filament \"" + name + "\" inherits from unknown preset \"" - + inherits_name + "\""; - BOOST_LOG_TRIVIAL(error) << msg; - m_stats.issues.push_back(make_error(-1, "FILAMENT_UNKNOWN_ANCESTOR", msg)); - any_error = true; - resolved = true; - } + std::string msg = "Filament \"" + name + "\" inherits from non-filament preset \"" + + inherits_name + "\""; + BOOST_LOG_TRIVIAL(error) << msg; + m_stats.issues.push_back(make_error(-1, "FILAMENT_UNKNOWN_ANCESTOR", msg)); + any_error = true; + resolved = true; } } } From 44ed90dfeb3e9e710d96423a2b291bb6c860c6d3 Mon Sep 17 00:00:00 2001 From: joyx-ubuntu Date: Thu, 28 May 2026 19:16:04 +0800 Subject: [PATCH 18/30] fix(slice-engine): enforce U1 printer preset, fail if not found - apply_printer_preset_config() now uses build_full_print_config() to resolve the complete printer config (defaults + system preset + project overlay) and merge missing keys into m_config. If system presets are unavailable, report PRINTER_PRESET_MISSING as a fatal error. - Restore FullPrintConfig::defaults() application before load_3mf() as baseline, so build_full_print_config() has a complete config to resolve on top of. - Remove blind FullPrintConfig defaults from apply_printer_preset_config (now handled at run() level) and dead guess_nozzle_str helper. - Validate critical printer parameters (printable_area, printable_height) after merge and report CONFIG_MISSING errors if absent. --- src/orca-slice-engine/SliceEngine.cpp | 75 ++++++++++++--------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index d9f0a66d4ae..0b9a388f990 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -63,10 +63,9 @@ bool SliceEngine::run() { m_output_path = generate_output_path(m_cfg.input_file, m_cfg.output_base, m_cfg.plate_id, m_cfg.format, m_cfg.single_plate); - // Apply FullPrintConfig defaults as baseline so every config key has - // a valid value before the 3MF overlays project settings on top. - // This mirrors PresetBundle::full_fff_config() and prevents - // Print::apply() crashes on missing options. + // Apply FullPrintConfig defaults as baseline BEFORE the 3MF overlays + // project settings on top. This ensures every config key has a valid value + // and the build_full_print_config() resolution works correctly. m_config.apply(FullPrintConfig::defaults()); bool load_ok = load_3mf(); @@ -475,54 +474,32 @@ void SliceEngine::validate_presets() void SliceEngine::apply_printer_preset_config() { - // Try to merge the printer preset config (system or project-embedded) - // into m_config to get machine-specific values (printable_area, - // printable_height, etc.). - // - // FullPrintConfig defaults are applied separately before load_3mf() - // to provide a baseline for all config keys. - if (!m_presets_available || !m_preset_bundle) - return; - if (!m_config.has("printer_settings_id")) - return; - - const std::string printer_name = m_config.opt_string("printer_settings_id"); - if (printer_name.empty()) + if (!m_presets_available || !m_preset_bundle) { + std::string msg = "System presets not available; cannot verify printer configuration."; + BOOST_LOG_TRIVIAL(error) << msg; + m_any_error = true; + set_error_type(EXIT_VALIDATION_ERROR); + m_stats.error_message = msg; + m_stats.issues.push_back(make_error(-1, "PRINTER_PRESET_MISSING", msg)); return; - - // Look up the printer preset: try system first, then project-embedded. - const Preset* printer_preset = m_preset_bundle->printers.find_preset(printer_name, false); - if (!printer_preset || printer_preset->name != printer_name) { - for (auto* pp : m_project_presets) { - if (pp && pp->name == printer_name && pp->type == Preset::TYPE_PRINTER) { - printer_preset = pp; - break; - } - } } - if (!printer_preset) { - BOOST_LOG_TRIVIAL(info) << "Printer preset '" << printer_name - << "' not found; using defaults only"; - return; - } + // Build a fully-resolved config via the engine's own config builder. + // This correctly resolves printer preset inheritance chains and includes + // all defaults. We merge back only nil keys so project config wins. + DynamicPrintConfig resolved = build_full_print_config(); + BOOST_LOG_TRIVIAL(info) << "Applying printer preset config"; - BOOST_LOG_TRIVIAL(info) << "Applying printer preset config: " << printer_name; - // Only fill keys that are missing (nil) in m_config — do not overwrite - // values already set by the project config or FullPrintConfig defaults. - // This matches the semantics of substitute_filament_params(). - for (auto it = printer_preset->config.cbegin(); - it != printer_preset->config.cend(); ++it) { + // Only fill nil/missing values — project config takes precedence. + for (auto it = resolved.cbegin(); + it != resolved.cend(); ++it) { const auto& key = it->first; auto* dst_opt = m_config.option(key, false); if (!dst_opt) continue; - // Scalar options: only copy if nil. if (dst_opt->is_scalar()) { if (!dst_opt->is_nil()) continue; dst_opt->set(it->second.get()); } else { - // Vector options: copy per-element only if nil at that index, - // matching substitute_filament_params() per-extruder semantics. auto* dst_vec = dynamic_cast(dst_opt); auto* src_vec = dynamic_cast(it->second.get()); if (!dst_vec || !src_vec) continue; @@ -532,6 +509,22 @@ void SliceEngine::apply_printer_preset_config() } } } + + // Verify critical printer parameters are now valid. + struct { const char* key; const char* label; } critical[] = { + {"printable_area", "Printable area"}, + {"printable_height", "Printable height"}, + }; + for (auto& c : critical) { + if (!m_config.has(c.key)) { + BOOST_LOG_TRIVIAL(error) << "Critical config key missing: " << c.label; + m_any_error = true; + set_error_type(EXIT_VALIDATION_ERROR); + m_stats.issues.push_back(make_error(-1, + std::string("CONFIG_MISSING_") + c.key, + std::string("Missing critical config: ") + c.label)); + } + } } bool SliceEngine::has_inline_filament_config(int ext_idx) From 228345071113e2818ea3cfa92882a4808f629646 Mon Sep 17 00:00:00 2001 From: joyx-ubuntu Date: Thu, 28 May 2026 19:42:04 +0800 Subject: [PATCH 19/30] fix(slice-engine): reject slicing when printer params remain at defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - After preset merge, detect whether printable_area and printable_height are still at FullPrintConfig defaults (200x200 rect, 100.0mm). If so, the U1 preset was not applied — report PRINTER_PRESET_NOT_APPLIED and fail slicing. - Only modifies the engine, no changes to libslic3r. --- src/orca-slice-engine/SliceEngine.cpp | 44 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 0b9a388f990..bcd5e3039bb 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -510,20 +510,40 @@ void SliceEngine::apply_printer_preset_config() } } - // Verify critical printer parameters are now valid. - struct { const char* key; const char* label; } critical[] = { - {"printable_area", "Printable area"}, - {"printable_height", "Printable height"}, - }; - for (auto& c : critical) { - if (!m_config.has(c.key)) { - BOOST_LOG_TRIVIAL(error) << "Critical config key missing: " << c.label; + // Verify printer-specific parameters have been overridden by the U1 + // preset and are NOT still at FullPrintConfig defaults. + // FullPrintConfig sets printable_area = [(0,0),(200,0),(200,200),(0,200)] + // and printable_height = 100.0 as generic placeholders. If these survive + // the preset merge, the U1 preset did not take effect. + { + auto fail = [&](const std::string& detail) { + std::string msg = "Printer configuration incomplete: " + detail + + ". The U1 printer preset was not applied correctly. " + "Verify the resources directory contains Snapmaker U1 machine profiles."; + BOOST_LOG_TRIVIAL(error) << msg; m_any_error = true; set_error_type(EXIT_VALIDATION_ERROR); - m_stats.issues.push_back(make_error(-1, - std::string("CONFIG_MISSING_") + c.key, - std::string("Missing critical config: ") + c.label)); - } + m_stats.error_message = msg; + m_stats.issues.push_back(make_error(-1, "PRINTER_PRESET_NOT_APPLIED", msg)); + }; + + // printable_area: default is 4-point 200x200 rect + auto* pa = m_config.option("printable_area"); + if (!pa || pa->values.size() != 4) { + fail("printable_area missing or wrong format"); + } else { + bool is_default = + (pa->values[0].x() == 0.0 && pa->values[0].y() == 0.0) && + (pa->values[1].x() == 200.0 && pa->values[1].y() == 0.0) && + (pa->values[2].x() == 200.0 && pa->values[2].y() == 200.0) && + (pa->values[3].x() == 0.0 && pa->values[3].y() == 200.0); + if (is_default) fail("printable_area is still the 200x200 default"); + } + + // printable_height: default is 100.0 + auto* ph = m_config.option("printable_height"); + if (!ph || ph->value == 100.0) + fail("printable_height is still the 100.0 default"); } } From 4e6f54eec2fcf180df9773c2e362a1170290d90f Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 12:03:39 +0800 Subject: [PATCH 20/30] Refactor cloud engine preset substitution: add printer preset enforcement, CLI granularity, resource path auto-detection - EngineConfig: remove enforce_official_presets/clear_custom_gcode/data_dir (dead), add substitute_printer flag for printer preset substitution control - CLI: add --allow-custom-printer-presets and --allow-custom-filament-presets for per-category preset enforcement; remove -d/--data-dir, --keep-custom-gcode, --no-filament-substitution (no backward compat) - Resource path: auto-detect ../resources (Ubuntu packaging) before ./resources fallback and ORCA_RESOURCES env var - validate_printer_official(): check printer preset via direct file lookup under resources/profiles/Snapmaker/machine/ since PresetBundle::find_preset binary search misses Snapmaker system presets - substitute_printer_params(): load official parent config from disk and fill nil values (includes G-code naturally via preset substitution) - validate_filament_official(): add file-based fallback for Snapmaker and OrcaFilamentLibrary filaments missed by find_preset - Remove apply_official_presets() (G-code now handled by printer substitution) - Simplify load_system_presets() (always resources/profiles, no --data-dir) - Suppress libslic3r log spam during preset loading/validation via set_logging_enabled(false) --- src/orca-slice-engine/SliceEngine.cpp | 284 ++++++++++++++++++-------- src/orca-slice-engine/SliceEngine.hpp | 14 +- src/orca-slice-engine/Utils.cpp | 12 +- src/orca-slice-engine/main.cpp | 34 +-- 4 files changed, 228 insertions(+), 116 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index bcd5e3039bb..e667c804ba8 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -76,24 +76,32 @@ bool SliceEngine::run() { // Config & preset validation (desktop parity) validate_config(); + + // Suppress libslic3r log spam during system preset loading and validation + boost::log::core::get()->set_logging_enabled(false); load_system_presets(); validate_presets(); + boost::log::core::get()->set_logging_enabled(true); + + // Printer preset substitution (includes G-code from official printer). + // When substitute_printer is true (default), force substitution to official. + // When false (--allow-custom-printer-presets), accept custom with warning. + if (!validate_printer_official(m_cfg.substitute_printer)) { + build_statistics(); + return false; + } + apply_printer_preset_config(); // Filament validation: always run to catch truly invalid presets. // When substitute_filaments is true (default), enforce official-only policy. - // When false (--allow-custom-presets), validate structural soundness but + // When false (--allow-custom-filament-presets), validate structural soundness but // accept custom filaments with a warning. if (!validate_filament_official(m_cfg.substitute_filaments)) { build_statistics(); return false; } - // Strip custom G-code blocks for cloud safety - if (m_cfg.clear_custom_gcode) { - apply_official_presets(); - } - // Block slicing if printer model is not Snapmaker U1 if (!validate_printer_model()) { build_statistics(); @@ -313,19 +321,13 @@ void SliceEngine::validate_config() void SliceEngine::load_system_presets() { - // Determine profiles directory: --data-dir takes precedence, - // otherwise derive from resources_dir/profiles/ - std::string profiles_path; - if (!m_cfg.data_dir.empty()) { - profiles_path = m_cfg.data_dir; - } else { - const std::string res_dir = Slic3r::resources_dir(); - if (res_dir.empty()) { - BOOST_LOG_TRIVIAL(info) << "No resources directory set; skipping preset validation"; - return; - } - profiles_path = res_dir + "/profiles"; + // Always derive profiles path from resources_dir/profiles/ + const std::string res_dir = Slic3r::resources_dir(); + if (res_dir.empty()) { + BOOST_LOG_TRIVIAL(info) << "No resources directory set; skipping preset validation"; + return; } + std::string profiles_path = res_dir + "/profiles"; boost::filesystem::path profiles_dir(profiles_path); if (!boost::filesystem::exists(profiles_dir) || @@ -635,6 +637,21 @@ bool SliceEngine::validate_filament_official(bool enforce) for (int i = 0; i < num_filaments; ++i) { const std::string& name = filament_ids->values[i]; + // Check if this is an official filament by looking for its JSON file + // under resources/profiles/Snapmaker/filament/ or OrcaFilamentLibrary/filament/ + // (find_preset may miss some system presets due to binary search ordering) + auto is_official_file = [&](const std::string& preset_name) -> bool { + const std::string res = Slic3r::resources_dir(); + if (res.empty()) return false; + // Snapmaker filament + if (boost::filesystem::exists(res + "/profiles/Snapmaker/filament/" + preset_name + ".json")) + return true; + // OrcaFilamentLibrary (Generic filaments) + if (boost::filesystem::exists(res + "/profiles/" + PresetBundle::ORCA_FILAMENT_LIBRARY + "/filament/" + preset_name + ".json")) + return true; + return false; + }; + // Case 1: Direct system preset match if (Preset* sys = find_in_system(name)) { if (is_official_preset(*sys)) { @@ -645,6 +662,12 @@ bool SliceEngine::validate_filament_official(bool enforce) continue; } + // Case 1b: File-based check — preset exists on disk but not + // found via find_preset (known issue with binary search ordering) + if (is_official_file(name)) { + continue; // OK + } + // Case 2: Not a direct system match — walk the inheritance chain Preset* current = find_in_project(name); if (!current) { @@ -842,92 +865,181 @@ bool SliceEngine::validate_printer_model() return true; } -void SliceEngine::apply_official_presets() +bool SliceEngine::validate_printer_official(bool enforce) { - // Find the official Snapmaker printer preset to source default G-code blocks. - // Mirrors the filament substitution logic: walk the inherits chain upward, - // fall back to nozzle-diameter-matched U1 preset if no official ancestor found. - const Preset* official_printer = nullptr; + auto* printer_id = m_config.option("printer_settings_id"); + if (!printer_id || printer_id->value.empty()) + return true; + + const std::string& name = printer_id->value; + const std::string res_dir = Slic3r::resources_dir(); + // Helper: check if a preset name matches an official Snapmaker + // printer preset by looking for its JSON file under + // resources/profiles/Snapmaker/machine/ + auto is_official_machine = [&](const std::string& preset_name) -> bool { + if (res_dir.empty()) return false; + return boost::filesystem::exists( + res_dir + "/profiles/Snapmaker/machine/" + preset_name + ".json"); + }; + + // Case 1: The printer_settings_id directly matches an official + // Snapmaker preset file — no substitution needed. + if (is_official_machine(name)) { + return true; + } + + // Case 2: Not directly official. Look for the preset in + // project-embedded presets or system presets and walk the + // inherits chain to find an official ancestor. + const Preset* current = nullptr; if (m_presets_available && m_preset_bundle) { - auto& bundle = *m_preset_bundle; + current = m_preset_bundle->printers.find_preset(name, false); + } + if (!current) { + for (auto* pp : m_project_presets) { + if (pp && pp->name == name && pp->type == Preset::TYPE_PRINTER) { + current = pp; + break; + } + } + } - // Check whether a system preset is "official" (Snapmaker vendor) - auto is_official = [](const Preset& p) -> bool { - return p.vendor && p.vendor->name == PresetBundle::SM_BUNDLE; - }; + if (!current) { + if (!enforce) { + BOOST_LOG_TRIVIAL(warning) << "Printer preset \"" << name + << "\" not found in system presets; accepted in allow-custom mode"; + m_stats.issues.push_back(make_warning(-1, "PRINTER_CUSTOM_NOT_FOUND", + std::string("Printer preset \"") + name + "\" not found in system presets")); + return true; + } + std::string msg = "Printer preset \"" + name + "\" is not a recognized preset"; + BOOST_LOG_TRIVIAL(error) << msg; + m_any_error = true; + set_error_type(EXIT_VALIDATION_ERROR); + m_stats.error_message = msg; + m_stats.issues.push_back(make_error(-1, "PRINTER_UNKNOWN", msg)); + return false; + } - auto* printer_id = m_config.option("printer_settings_id"); - std::string printer_name = printer_id ? printer_id->value : ""; + // Walk the inherits chain to find official Snapmaker ancestor + std::set visited; + const Preset* walk = current; + while (walk) { + std::string inherits_name = walk->inherits(); + if (inherits_name.empty()) break; + if (!visited.insert(inherits_name).second) { + std::string msg = "Circular inheritance in printer preset \"" + name + "\""; + BOOST_LOG_TRIVIAL(error) << msg; + m_any_error = true; + set_error_type(EXIT_VALIDATION_ERROR); + m_stats.issues.push_back(make_error(-1, "PRINTER_CIRCULAR_INHERITS", msg)); + return false; + } - // Look up the printer preset — system first, then project-embedded - if (!printer_name.empty()) { - const Preset* sys = bundle.printers.find_preset(printer_name, false); - const Preset* current = (sys && sys->name == printer_name) ? sys : nullptr; + if (is_official_machine(inherits_name)) { + if (enforce) { + substitute_printer_params(name, inherits_name); + } else { + BOOST_LOG_TRIVIAL(warning) << "Printer preset \"" << name + << "\" is a custom preset (not official)"; + m_stats.issues.push_back(make_warning(-1, "PRINTER_CUSTOM", + std::string("Printer preset \"") + name + "\" is a custom preset (not official)")); + } + return true; + } - // Walk inherits chain to find official Snapmaker ancestor - std::set visited; - while (current) { - if (is_official(*current)) { - official_printer = current; + const Preset* parent = nullptr; + if (m_presets_available && m_preset_bundle) { + parent = m_preset_bundle->printers.find_preset(inherits_name, false); + } + if (!parent) { + for (auto* pp : m_project_presets) { + if (pp && pp->name == inherits_name && pp->type == Preset::TYPE_PRINTER) { + parent = pp; break; } - std::string parent_name = current->inherits(); - if (parent_name.empty() || !visited.insert(parent_name).second) - break; - current = bundle.printers.find_preset(parent_name, false); } } + if (!parent) break; + walk = parent; + } - // Fallback: match by nozzle diameter - if (!official_printer) { - std::string nozzle_str = "0.4"; - auto* nd = m_config.option("nozzle_diameter"); - if (nd && !nd->values.empty()) { - double dia = nd->values[0]; - if (dia < 0.3) nozzle_str = "0.2"; - else if (dia < 0.5) nozzle_str = "0.4"; - else if (dia < 0.7) nozzle_str = "0.6"; - else nozzle_str = "0.8"; - } - std::string fallback = "Snapmaker U1 (" + nozzle_str + " nozzle)"; - official_printer = bundle.printers.find_preset(fallback, true); - } + if (!enforce) { + BOOST_LOG_TRIVIAL(warning) << "Printer preset \"" << name + << "\" is a custom preset (not official)"; + m_stats.issues.push_back(make_warning(-1, "PRINTER_CUSTOM", + std::string("Printer preset \"") + name + "\" is a custom preset (not official)")); + return true; } - // Replace every G-code key the official printer preset provides. - // Any other G-code-related key still gets cleared for cloud safety. - std::set replaced; - if (official_printer) { - for (auto it = official_printer->config.cbegin(); - it != official_printer->config.cend(); ++it) { - const std::string& key = it->first; - if (key.find("gcode") == std::string::npos && key != "print_host") - continue; - if (!m_config.has(key)) continue; + std::string msg = "Printer preset \"" + name + + "\" is not derived from any Snapmaker official printer preset"; + BOOST_LOG_TRIVIAL(error) << msg; + m_any_error = true; + set_error_type(EXIT_VALIDATION_ERROR); + m_stats.error_message = msg; + m_stats.issues.push_back(make_error(-1, "PRINTER_NO_OFFICIAL_ANCESTOR", msg)); + return false; +} + +void SliceEngine::substitute_printer_params(const std::string& original_name, + const std::string& parent_name) +{ + BOOST_LOG_TRIVIAL(info) << "Substituting printer preset \"" << original_name + << "\" with official parent \"" << parent_name << "\""; + + m_config.set_key_value("printer_settings_id", + new ConfigOptionString(parent_name)); + + // Load the parent preset config from the Snapmaker machine directory + std::string parent_path = Slic3r::resources_dir() + + "/profiles/Snapmaker/machine/" + parent_name + ".json"; - std::string value; - auto* opt = official_printer->config.option(key); - if (opt) value = opt->value; - m_config.set_key_value(key, new ConfigOptionString(value)); - replaced.insert(key); - m_stats.issues.push_back(make_tip(-1, "GCODE_REPLACED", - std::string("G-code '") + key + "' replaced with official default")); + DynamicPrintConfig parent_cfg; + std::map key_values; + std::string reason; + try { + ConfigSubstitutions subs = parent_cfg.load_from_json( + parent_path, ForwardCompatibilitySubstitutionRule::EnableSilent, + key_values, reason); + + // Copy printer_model from parent if available + auto* pm = parent_cfg.option("printer_model"); + if (pm && m_config.has("printer_model")) { + m_config.set_key_value("printer_model", + new ConfigOptionString(pm->value)); + } + + // Fill nil/missing values from parent config + for (auto it = parent_cfg.cbegin(); it != parent_cfg.cend(); ++it) { + const auto& key = it->first; + auto* dst_opt = m_config.option(key, false); + if (!dst_opt) continue; + + if (dst_opt->is_scalar()) { + if (!dst_opt->is_nil()) continue; + dst_opt->set(it->second.get()); + } else { + auto* dst_vec = dynamic_cast(dst_opt); + auto* src_vec = dynamic_cast( + it->second.get()); + if (!dst_vec || !src_vec) continue; + for (size_t i = 0; i < dst_vec->size() && i < src_vec->size(); ++i) { + if (dst_vec->is_nil(i)) + dst_vec->set_at(src_vec, i, i); + } + } } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) + << "Failed to load parent preset config from " << parent_path + << ": " << e.what(); } - // Clear any remaining G-code keys not covered by the official printer preset - constexpr const char* known_gcode_keys[] = { - "start_gcode", "end_gcode", "layer_gcode", - "between_objects_gcode", "toolchange_gcode", - }; - for (const char* key : known_gcode_keys) { - if (!m_config.has(key)) continue; - if (replaced.count(key)) continue; - m_config.set_key_value(key, new ConfigOptionString("")); - m_stats.issues.push_back(make_tip(-1, "GCODE_CLEARED", - std::string("Custom G-code '") + key + "' cleared for cloud safety")); - } + m_stats.issues.push_back(make_warning(-1, "PRINTER_SUBSTITUTED", + std::string("Printer preset \"") + original_name + + "\" substituted with official preset \"" + parent_name + "\"")); } // ============================================================================ diff --git a/src/orca-slice-engine/SliceEngine.hpp b/src/orca-slice-engine/SliceEngine.hpp index 6458d325b44..10aca295b32 100644 --- a/src/orca-slice-engine/SliceEngine.hpp +++ b/src/orca-slice-engine/SliceEngine.hpp @@ -28,13 +28,11 @@ struct EngineConfig { OutputFormat format = OutputFormat::GCODE_3MF; bool single_plate = false; std::string temp_dir; // temp directory for intermediate gcode files - std::string data_dir; // --data-dir, custom system presets path (empty = auto) int timeout_seconds = 0; // 0 = no timeout; cloud service sets based on file size int max_size_mb = 200; // 0 = no limit; max input file size in megabytes std::string cancel_file; // watchdog file path for external cancellation - bool enforce_official_presets = true; // P0-2: replace user config with official presets - bool substitute_filaments = true; // whether to check & substitute filament with official parent - bool clear_custom_gcode = true; // whether to strip custom G-code blocks for cloud safety + bool substitute_printer = true; // whether to substitute printer preset with official parent + bool substitute_filaments = true; // whether to substitute filament presets with official parent }; // Intermediate result for a single plate during the pipeline @@ -69,10 +67,6 @@ struct PlateSliceResult { // applied in apply_model() before passing to Print::apply(). // // Key design decisions: -// - Fresh Print object per retry attempt (no explicit dtor / placement-new) -// - Per-plate error handling via report_error() helper -// - try-catch boundary at process_plate() prevents one plate from crashing the job -// - Custom G-code stripped for cloud safety (apply_official_presets) // class SliceEngine { public: @@ -101,7 +95,7 @@ class SliceEngine { void validate_config(); void load_system_presets(); void validate_presets(); - void apply_official_presets(); + bool validate_printer_official(bool enforce = true); Slic3r::DynamicPrintConfig build_full_print_config(); bool validate_filament_official(bool enforce = true); bool has_inline_filament_config(int ext_idx); @@ -109,6 +103,8 @@ class SliceEngine { void substitute_filament_params(Slic3r::ConfigOptionStrings* filament_ids, int ext_idx, const Slic3r::Preset& official_parent, const std::string& original_name); + void substitute_printer_params(const std::string& original_name, + const std::string& parent_name); bool validate_printer_model(); bool validate_input(); void process_plate(int plate_id); diff --git a/src/orca-slice-engine/Utils.cpp b/src/orca-slice-engine/Utils.cpp index a893d821884..d21095e0c9e 100644 --- a/src/orca-slice-engine/Utils.cpp +++ b/src/orca-slice-engine/Utils.cpp @@ -52,18 +52,18 @@ void print_usage(const char* program_name) { std::cout << " -f, --format Output format: gcode | gcode.3mf (default: gcode.3mf)" << std::endl; std::cout << " Note: All plates always use gcode.3mf" << std::endl; std::cout << " -r, --resources Resources directory containing printer profiles" << std::endl; - std::cout << " -d, --data-dir System presets directory (default: /profiles)" << std::endl; + std::cout << " If not set, auto-detected from binary location" << std::endl; + std::cout << " (../resources/, then ./resources/, then $ORCA_RESOURCES)" << std::endl; std::cout << " -j, --json [file] Output slice statistics as JSON to specified file" << std::endl; std::cout << " If not specified, JSON is auto-saved next to the output" << std::endl; std::cout << " -t, --timeout Slicing timeout in seconds (0 = no limit)" << std::endl; std::cout << " --max-size Max input file size in MB (default: 200, 0 = no limit)" << std::endl; std::cout << " --cancel-file Watchdog file for external cancellation" << std::endl; std::cout << " If the file is created, slicing is cancelled" << std::endl; - std::cout << " --allow-custom-presets Disable all enforcement (filament + G-code)" << std::endl; - std::cout << " --no-filament-substitution Skip filament official compliance check" << std::endl; - std::cout << " (default: check & substitute)" << std::endl; - std::cout << " --keep-custom-gcode Keep custom G-code blocks unchanged" << std::endl; - std::cout << " (default: clear all custom G-code)" << std::endl; + std::cout << " --allow-custom-presets Allow both custom printer and filament presets" << std::endl; + std::cout << " --allow-custom-printer-presets Allow custom printer presets" << std::endl; + std::cout << " --allow-custom-filament-presets Allow custom filament presets" << std::endl; + std::cout << " (default: all presets substituted to official)" << std::endl; std::cout << " --log Enable log file output (auto-saved next to the output)" << std::endl; std::cout << " --log-file Specify log file path (implies --log)" << std::endl; std::cout << " -v, --verbose Enable verbose logging" << std::endl; diff --git a/src/orca-slice-engine/main.cpp b/src/orca-slice-engine/main.cpp index 4f241367ccf..5b17eaf290c 100644 --- a/src/orca-slice-engine/main.cpp +++ b/src/orca-slice-engine/main.cpp @@ -93,7 +93,8 @@ CliArgs parse_args(int argc, char* argv[]) { } } else if ((arg == "-d" || arg == "--data-dir") && i + 1 < argc) { - args.engine_cfg.data_dir = argv[++i]; + BOOST_LOG_TRIVIAL(warning) << "--data-dir is deprecated and ignored; presets are always loaded from /profiles"; + ++i; // consume the value } else if ((arg == "-t" || arg == "--timeout") && i + 1 < argc) { try { @@ -120,15 +121,14 @@ CliArgs parse_args(int argc, char* argv[]) { args.engine_cfg.cancel_file = argv[++i]; } else if (arg == "--allow-custom-presets") { - args.engine_cfg.enforce_official_presets = false; - args.engine_cfg.substitute_filaments = false; - args.engine_cfg.clear_custom_gcode = false; - } - else if (arg == "--no-filament-substitution") { + args.engine_cfg.substitute_printer = false; args.engine_cfg.substitute_filaments = false; } - else if (arg == "--keep-custom-gcode") { - args.engine_cfg.clear_custom_gcode = false; + else if (arg == "--allow-custom-printer-presets") { + args.engine_cfg.substitute_printer = false; + } + else if (arg == "--allow-custom-filament-presets") { + args.engine_cfg.substitute_filaments = false; } else if ((arg == "-f" || arg == "--format") && i + 1 < argc) { std::string fmt = argv[++i]; @@ -249,16 +249,20 @@ int main(int argc, char* argv[]) { if (!resources_dir.empty()) { set_resources_dir(resources_dir); } else { - boost::filesystem::path exe_path = boost::dll::program_location(); - boost::filesystem::path resource_path = exe_path.parent_path() / "resources"; - if (boost::filesystem::exists(resource_path)) { - set_resources_dir(resource_path.string()); + // Auto-detect: prefer ../resources (Ubuntu packaging: bin/orca-slice-engine -> x/resources/) + // fall back to ./resources (development layout) + boost::filesystem::path exe_dir = boost::dll::program_location().parent_path(); + boost::filesystem::path parent_resources = exe_dir.parent_path() / "resources"; + boost::filesystem::path local_resources = exe_dir / "resources"; + if (boost::filesystem::exists(parent_resources)) { + set_resources_dir(parent_resources.string()); + } else if (boost::filesystem::exists(local_resources)) { + set_resources_dir(local_resources.string()); } else { const char* env = std::getenv("ORCA_RESOURCES"); if (env) { - std::string env_resources(env); - set_resources_dir(env_resources); - BOOST_LOG_TRIVIAL(info) << "Resources directory (from env): " << env_resources; + set_resources_dir(env); + BOOST_LOG_TRIVIAL(info) << "Resources directory (from env): " << env; } else { BOOST_LOG_TRIVIAL(warning) << "No resources directory specified. Using default preset loading."; } From 6ce4898e6b8ca6e70687b11f3cf175c2af49127b Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 13:11:27 +0800 Subject: [PATCH 21/30] Fix filament file check: add @System suffix for OrcaFilamentLibrary presets Generic filaments like 'Generic PETG HF' are stored as 'Generic PETG HF @System.json' under OrcaFilamentLibrary/filament/. The file-based fallback now also checks the @System suffix variant. --- src/orca-slice-engine/SliceEngine.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index e667c804ba8..88744f8aee3 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -646,8 +646,11 @@ bool SliceEngine::validate_filament_official(bool enforce) // Snapmaker filament if (boost::filesystem::exists(res + "/profiles/Snapmaker/filament/" + preset_name + ".json")) return true; - // OrcaFilamentLibrary (Generic filaments) - if (boost::filesystem::exists(res + "/profiles/" + PresetBundle::ORCA_FILAMENT_LIBRARY + "/filament/" + preset_name + ".json")) + // OrcaFilamentLibrary (Generic filaments) — also try @System suffix + const std::string orca_dir = res + "/profiles/" + PresetBundle::ORCA_FILAMENT_LIBRARY + "/filament/"; + if (boost::filesystem::exists(orca_dir + preset_name + ".json")) + return true; + if (boost::filesystem::exists(orca_dir + preset_name + " @System.json")) return true; return false; }; From f8755c24c659542e3eacc199f462fae6eac4824d Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 14:17:07 +0800 Subject: [PATCH 22/30] Fix multi-plate extruder mapping: stop truncating filament arrays for single-extruder plates The per-plate extruder trimming logic was truncating all filament arrays (type, color, diameter, etc.) to size 1 when only one extruder was used on a plate. This broke extruder assignment for model volumes referencing non-zero extruder IDs (e.g., extruder 3 on a plate that only uses extruder 3). Fix: only disable the prime/wipe tower for single-extruder plates, but keep filament arrays at their full size so volume-to-extruder mapping works correctly for any extruder index. --- src/orca-slice-engine/SliceEngine.cpp | 41 ++++++--------------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 88744f8aee3..775210966aa 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -1558,40 +1558,15 @@ bool SliceEngine::apply_model(int plate_id, Print& print, const Vec3d& origin) { } if (used_extruders.size() <= 1 && num_filaments > 1) { - BOOST_LOG_TRIVIAL(info) << "Trimming filament config from " << num_filaments - << " to 1 to match single-extruder model"; + // Single extruder model with multiple filaments configured. + // Disable the prime/wipe tower to prevent tool change errors, + // but keep filament arrays intact so model volumes referencing + // non-zero extruders still map correctly. + BOOST_LOG_TRIVIAL(info) << "Disabling prime tower for single-extruder plate " + << (plate_id + 1) << " (used extruders: " + << (used_extruders.empty() ? "none" : std::to_string(*used_extruders.begin())) + << ", filaments configured: " << num_filaments << ")"; merged_config.set_key_value("enable_prime_tower", new ConfigOptionBool(false)); - - // Truncate all filament-related array options to 1 entry. - // This prevents Print::has_wipe_tower() from returning true due to - // filament_diameter.size() > 1, which is the root cause of the - // "append_tcr was asked to do a toolchange it didn't expect" error. - // flush_volumes_matrix (N*N flat vector) and wiping_volumes_extruders - // must also be trimmed to keep the config internally consistent: - // a 5*5 matrix with only 1 extruder would mismatch sqrt(size) later. - constexpr const char* trim_keys[] = { - "filament_diameter", - "filament_density", - "filament_cost", - "filament_colour", - "filament_type", - "filament_is_support", - "filament_settings_id", - "nozzle_diameter", - "flush_volumes_matrix", - "wiping_volumes_extruders", - }; - for (const char* key : trim_keys) { - auto* opt = merged_config.option(key, true); - if (!opt) continue; - if (auto* fs = dynamic_cast(opt)) { - if (!fs->values.empty()) { fs->values.resize(1); merged_config.set_key_value(key, new ConfigOptionFloats(fs->values)); } - } else if (auto* ss = dynamic_cast(opt)) { - if (!ss->values.empty()) { ss->values.resize(1); merged_config.set_key_value(key, new ConfigOptionStrings(ss->values)); } - } else if (auto* bs = dynamic_cast(opt)) { - if (!bs->values.empty()) { bs->values.resize(1); merged_config.set_key_value(key, new ConfigOptionBools(bs->values)); } - } - } } else if (used_extruders.size() <= 1) { BOOST_LOG_TRIVIAL(info) << "Disabling prime tower (single extruder model)"; merged_config.set_key_value("enable_prime_tower", new ConfigOptionBool(false)); From 7280aed5e9c2fad4568db7d8ca637f16f37ce516 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 14:30:23 +0800 Subject: [PATCH 23/30] Refactor: extract shared helpers, RAII log suppressor, doc config mutation order P1: - ScopedLogSuppressor: RAII guard replacing bare set_logging_enabled(false/true), ensures log state is restored even on exception during preset loading - run() header comment documenting m_config mutation pipeline order P2: - fill_nil_from(): extract duplicated is_scalar/is_nil config fill pattern used by substitute_printer_params() and apply_printer_preset_config() - Path constants: SNAPMK_MACHINE_DIR, SNAPMK_FILAMENT_DIR, ORCA_FILAMENT_DIR - is_official_machine_file() / is_official_filament_file(): replace inline file-exists lambdas in validate_printer_official() and validate_filament_official() --- src/orca-slice-engine/SliceEngine.cpp | 183 ++++++++++++++------------ 1 file changed, 100 insertions(+), 83 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 775210966aa..a6c98135873 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -36,6 +36,78 @@ constexpr double BED_AXES_TIP_RADIUS = 1.25; // 1/5, same as GUI's LOGICAL_PART_PLATE_GAP constexpr double LOGICAL_PART_PLATE_GAP = 0.2; +// RAII guard to temporarily suppress all boost::log output. +// Restores the previous logging state on destruction, ensuring +// that exceptions during the suppressed scope do not leave +// logging permanently disabled. +class ScopedLogSuppressor { +public: + ScopedLogSuppressor() { + boost::log::core::get()->set_logging_enabled(false); + } + ~ScopedLogSuppressor() { + boost::log::core::get()->set_logging_enabled(true); + } + ScopedLogSuppressor(const ScopedLogSuppressor&) = delete; + ScopedLogSuppressor& operator=(const ScopedLogSuppressor&) = delete; +}; + +// --- Config helpers --- + +// Fill nil/missing values in dst from src. Scalar options use set(); +// vector options use set_at() per index for nil slots only. +// Non-existent keys in dst are skipped. Used by preset substitution +// and printer config layering to fill gaps while preserving user values. +inline void fill_nil_from(DynamicPrintConfig& dst, const DynamicPrintConfig& src) +{ + for (auto it = src.cbegin(); it != src.cend(); ++it) { + const auto& key = it->first; + auto* dst_opt = dst.option(key, false); + if (!dst_opt) continue; + + if (dst_opt->is_scalar()) { + if (dst_opt->is_nil()) + dst_opt->set(it->second.get()); + } else { + auto* dst_vec = dynamic_cast(dst_opt); + auto* src_vec = dynamic_cast(it->second.get()); + if (!dst_vec || !src_vec) continue; + for (size_t i = 0; i < dst_vec->size() && i < src_vec->size(); ++i) + if (dst_vec->is_nil(i)) + dst_vec->set_at(src_vec, i, i); + } + } +} + +// --- Official preset file helpers --- + +// Directory constants for system preset files under resources/profiles/ +static const char* const SNAPMK_MACHINE_DIR = "/profiles/Snapmaker/machine/"; +static const char* const SNAPMK_FILAMENT_DIR = "/profiles/Snapmaker/filament/"; +static const char* const ORCA_FILAMENT_DIR() { // ORCA_FILAMENT_LIBRARY is a runtime string + static const std::string s = std::string("/profiles/") + PresetBundle::ORCA_FILAMENT_LIBRARY + "/filament/"; + return s.c_str(); +} + +// Check whether a machine name matches an official Snapmaker printer preset on disk. +inline bool is_official_machine_file(const std::string& preset_name) { + const std::string& res = Slic3r::resources_dir(); + if (res.empty()) return false; + return boost::filesystem::exists(res + SNAPMK_MACHINE_DIR + preset_name + ".json"); +} + +// Check whether a filament name matches an official preset on disk +// (Snapmaker or OrcaFilamentLibrary, including @System suffix for Generic filaments). +inline bool is_official_filament_file(const std::string& preset_name) { + const std::string& res = Slic3r::resources_dir(); + if (res.empty()) return false; + const std::string snap_path = res + SNAPMK_FILAMENT_DIR + preset_name + ".json"; + const std::string orca_dir = res + ORCA_FILAMENT_DIR(); + return boost::filesystem::exists(snap_path) + || boost::filesystem::exists(orca_dir + preset_name + ".json") + || boost::filesystem::exists(orca_dir + preset_name + " @System.json"); +} + // Check if a plate result indicates a wipe tower tool change mismatch. // CGAL/float differences on some platforms cause non-consecutive extruder // ID handling to fail during G-code export. @@ -63,9 +135,19 @@ bool SliceEngine::run() { m_output_path = generate_output_path(m_cfg.input_file, m_cfg.output_base, m_cfg.plate_id, m_cfg.format, m_cfg.single_plate); - // Apply FullPrintConfig defaults as baseline BEFORE the 3MF overlays - // project settings on top. This ensures every config key has a valid value - // and the build_full_print_config() resolution works correctly. + // --- Config layering & mutation order --- + // m_config is the shared project config from the 3MF. The following + // pipeline stages mutate it in order, each building on the previous: + // + // 1. FullPrintConfig::defaults() — baseline for all keys + // 2. load_3mf() — overlay 3MF project + embedded presets + // 3. load_system_presets() — (side-effect: populates m_preset_bundle) + // 4. validate_printer_official() — MAY replace printer_settings_id + fill nil from official + // 5. apply_printer_preset_config() — fill remaining nil printer keys from system preset + // 6. validate_filament_official() — MAY replace filament_settings_id + fill nil from official + // + // After these stages, m_config is a fully-resolved config ready for slicing. + // Per-plate overrides are applied separately in apply_model(). m_config.apply(FullPrintConfig::defaults()); bool load_ok = load_3mf(); @@ -78,10 +160,11 @@ bool SliceEngine::run() { validate_config(); // Suppress libslic3r log spam during system preset loading and validation - boost::log::core::get()->set_logging_enabled(false); - load_system_presets(); - validate_presets(); - boost::log::core::get()->set_logging_enabled(true); + { + ScopedLogSuppressor quiet; + load_system_presets(); + validate_presets(); + } // Printer preset substitution (includes G-code from official printer). // When substitute_printer is true (default), force substitution to official. @@ -492,25 +575,7 @@ void SliceEngine::apply_printer_preset_config() DynamicPrintConfig resolved = build_full_print_config(); BOOST_LOG_TRIVIAL(info) << "Applying printer preset config"; - // Only fill nil/missing values — project config takes precedence. - for (auto it = resolved.cbegin(); - it != resolved.cend(); ++it) { - const auto& key = it->first; - auto* dst_opt = m_config.option(key, false); - if (!dst_opt) continue; - if (dst_opt->is_scalar()) { - if (!dst_opt->is_nil()) continue; - dst_opt->set(it->second.get()); - } else { - auto* dst_vec = dynamic_cast(dst_opt); - auto* src_vec = dynamic_cast(it->second.get()); - if (!dst_vec || !src_vec) continue; - for (size_t i = 0; i < dst_vec->size() && i < src_vec->size(); ++i) { - if (dst_vec->is_nil(i)) - dst_vec->set_at(src_vec, i, i); - } - } - } + fill_nil_from(m_config, resolved); // Verify printer-specific parameters have been overridden by the U1 // preset and are NOT still at FullPrintConfig defaults. @@ -637,24 +702,6 @@ bool SliceEngine::validate_filament_official(bool enforce) for (int i = 0; i < num_filaments; ++i) { const std::string& name = filament_ids->values[i]; - // Check if this is an official filament by looking for its JSON file - // under resources/profiles/Snapmaker/filament/ or OrcaFilamentLibrary/filament/ - // (find_preset may miss some system presets due to binary search ordering) - auto is_official_file = [&](const std::string& preset_name) -> bool { - const std::string res = Slic3r::resources_dir(); - if (res.empty()) return false; - // Snapmaker filament - if (boost::filesystem::exists(res + "/profiles/Snapmaker/filament/" + preset_name + ".json")) - return true; - // OrcaFilamentLibrary (Generic filaments) — also try @System suffix - const std::string orca_dir = res + "/profiles/" + PresetBundle::ORCA_FILAMENT_LIBRARY + "/filament/"; - if (boost::filesystem::exists(orca_dir + preset_name + ".json")) - return true; - if (boost::filesystem::exists(orca_dir + preset_name + " @System.json")) - return true; - return false; - }; - // Case 1: Direct system preset match if (Preset* sys = find_in_system(name)) { if (is_official_preset(*sys)) { @@ -667,7 +714,7 @@ bool SliceEngine::validate_filament_official(bool enforce) // Case 1b: File-based check — preset exists on disk but not // found via find_preset (known issue with binary search ordering) - if (is_official_file(name)) { + if (is_official_filament_file(name)) { continue; // OK } @@ -875,20 +922,10 @@ bool SliceEngine::validate_printer_official(bool enforce) return true; const std::string& name = printer_id->value; - const std::string res_dir = Slic3r::resources_dir(); - - // Helper: check if a preset name matches an official Snapmaker - // printer preset by looking for its JSON file under - // resources/profiles/Snapmaker/machine/ - auto is_official_machine = [&](const std::string& preset_name) -> bool { - if (res_dir.empty()) return false; - return boost::filesystem::exists( - res_dir + "/profiles/Snapmaker/machine/" + preset_name + ".json"); - }; // Case 1: The printer_settings_id directly matches an official // Snapmaker preset file — no substitution needed. - if (is_official_machine(name)) { + if (is_official_machine_file(name)) { return true; } @@ -940,7 +977,7 @@ bool SliceEngine::validate_printer_official(bool enforce) return false; } - if (is_official_machine(inherits_name)) { + if (is_official_machine_file(inherits_name)) { if (enforce) { substitute_printer_params(name, inherits_name); } else { @@ -995,45 +1032,25 @@ void SliceEngine::substitute_printer_params(const std::string& original_name, m_config.set_key_value("printer_settings_id", new ConfigOptionString(parent_name)); - // Load the parent preset config from the Snapmaker machine directory + // Load the parent preset config from disk std::string parent_path = Slic3r::resources_dir() - + "/profiles/Snapmaker/machine/" + parent_name + ".json"; + + SNAPMK_MACHINE_DIR + parent_name + ".json"; DynamicPrintConfig parent_cfg; std::map key_values; std::string reason; try { - ConfigSubstitutions subs = parent_cfg.load_from_json( - parent_path, ForwardCompatibilitySubstitutionRule::EnableSilent, + parent_cfg.load_from_json(parent_path, + ForwardCompatibilitySubstitutionRule::EnableSilent, key_values, reason); // Copy printer_model from parent if available auto* pm = parent_cfg.option("printer_model"); - if (pm && m_config.has("printer_model")) { + if (pm && m_config.has("printer_model")) m_config.set_key_value("printer_model", new ConfigOptionString(pm->value)); - } - // Fill nil/missing values from parent config - for (auto it = parent_cfg.cbegin(); it != parent_cfg.cend(); ++it) { - const auto& key = it->first; - auto* dst_opt = m_config.option(key, false); - if (!dst_opt) continue; - - if (dst_opt->is_scalar()) { - if (!dst_opt->is_nil()) continue; - dst_opt->set(it->second.get()); - } else { - auto* dst_vec = dynamic_cast(dst_opt); - auto* src_vec = dynamic_cast( - it->second.get()); - if (!dst_vec || !src_vec) continue; - for (size_t i = 0; i < dst_vec->size() && i < src_vec->size(); ++i) { - if (dst_vec->is_nil(i)) - dst_vec->set_at(src_vec, i, i); - } - } - } + fill_nil_from(m_config, parent_cfg); } catch (const std::exception& e) { BOOST_LOG_TRIVIAL(warning) << "Failed to load parent preset config from " << parent_path From 6742109a25ada51437cf3d60b9cec796cf060419 Mon Sep 17 00:00:00 2001 From: zhangzhend0ng Date: Fri, 29 May 2026 16:13:06 +0800 Subject: [PATCH 24/30] =?UTF-8?q?bugfix:=20Mixed=20filament=20=E2=80=94=20?= =?UTF-8?q?3MF=20persistence,=20Local-Z=20settings,=20dialog=20panel=20fix?= =?UTF-8?q?es=20(#401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix 3MF persistence for Local-Z project settings Fixes #125. Add Local-Z Full Domain and related mixed-filament project settings to the project config and import allow-lists so they survive 3MF save/load and model import. * Load Local-Z settings from imported 3MF geometry Fix geometry-only 3MF import so saved Local-Z project settings override the current unsaved state even when importing into a non-empty scene. Mirror the print-tab Local-Z booleans as well so the UI does not drift back to the old preset values. * Keep Local-Z infill option in sync with subdivision mode Default Apply subdivision to infill to off while Subdivide Mix Layer is disabled, auto-enable it when subdivision is enabled, and clear dependent Local-Z child settings when subdivision is disabled so saved project config does not retain stale active child values. * bugfix: mix dialog error/warning panel oversized on first display - Reorder Show+Layout before SetLabel so hidden LB_AUTO_WRAP Label gets valid width for wrapping, preventing NSTextField from reporting overly wide intrinsicContentSize - Clean up i18n keys: drop colons (Filaments:/Target Color:), fix cycle card title Pattern→Filaments - Sync zh_CN translations * Refresh print tab after importing Local-Z settings After geometry-only 3MF import, reload the print tab when Local-Z settings were imported so the visible Subdivide Mix Layer controls match the project config used for slicing. * Do not import Local-Z process settings with geometry File -> Import 3MF should preserve the current project's process parameter state. Stop applying saved Local-Z project/print settings during geometry-only 3MF import; those settings are still loaded when opening a project with config. * Fix inherited plate bed type dirty state * i18n: add zh_CN translation for "Apply subdivision to infill" * docs: add inline comments explaining double-Layout macOS workaround in MixedFilamentDialog The error/warning labels use LB_AUTO_WRAP, which requires two Layout() passes on macOS: the first lands the panel's actual width after Show(), the second re-wraps the label text at the correct width. Without this, a single Layout can query a stale zero-width from CalcMin and produce an oversized dialog on first display. --------- Co-authored-by: ratdoux Co-authored-by: ZhangZheng <67276816+LuckZAE@users.noreply.github.com> --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 7 ++- src/libslic3r/Preset.cpp | 1 + src/libslic3r/PresetBundle.cpp | 6 ++ src/libslic3r/PrintConfig.cpp | 2 +- src/slic3r/GUI/MixedFilamentDialog.cpp | 17 +++--- src/slic3r/GUI/Tab.cpp | 60 +++++++++++++++---- tests/libslic3r/test_mixed_filament.cpp | 28 +++++++++ 7 files changed, 98 insertions(+), 23 deletions(-) diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index a585801ecf9..6cc731d73af 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15151,8 +15151,8 @@ msgstr "编辑混色" msgid "Order" msgstr "顺序" -msgid "Target Color:" -msgstr "目标色:" +msgid "Target Color" +msgstr "目标色" msgid "Swap filaments" msgstr "切换方向" @@ -15205,6 +15205,9 @@ msgstr "参与混色耗材颜色一致,无法混出更多颜色,请选用不 msgid "Full domain" msgstr "全域生效" +msgid "Apply subdivision to infill" +msgstr "应用到填充" + msgid "Invalid characters found. Only digits, square brackets ([ and ]), and commas (,) are allowed." msgstr "包含无效字符。仅允许输入数字、方括号([ 和])和逗号(,)。" diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f602d7814e8..67742c0cb85 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -907,6 +907,7 @@ static std::vector s_Preset_print_options { "seam_slope_type", "seam_slope_conditional", "scarf_angle_threshold", "scarf_joint_speed", "scarf_joint_flow_ratio", "seam_slope_start_height", "seam_slope_entire_loop", "seam_slope_min_length", "seam_slope_steps", "seam_slope_inner_walls", "scarf_overhang_threshold", "interlocking_beam", "interlocking_orientation", "interlocking_beam_layer_count", "interlocking_depth", "interlocking_boundary_avoidance", "interlocking_beam_width", "dithering_local_z_mode", + "dithering_local_z_whole_objects", "dithering_local_z_infill", "calib_flowrate_topinfill_special_order", }; diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 8cefdafde74..e93dfa024c5 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -80,6 +80,12 @@ static std::vector s_project_options { "mixed_filament_definitions", "mixed_color_layer_height_a", "mixed_color_layer_height_b", + "dithering_z_step_size", + "dithering_local_z_mode", + "dithering_local_z_whole_objects", + "dithering_local_z_infill", + "dithering_local_z_direct_multicolor", + "dithering_step_painted_zones_only", }; // SM_FEATURE: add Snapmaker machine as default diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 3487801cf73..179cae85ab4 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -4362,7 +4362,7 @@ void PrintConfigDef::init_fff_params() "This is enabled automatically with Subdivide Mix Layer. Turn it off to keep infill on the normal layer height.\n\n" "It can improve internal color mixing, but may add toolchanges and affect infill behavior."); def->mode = comAdvanced; - def->set_default_value(new ConfigOptionBool(true)); + def->set_default_value(new ConfigOptionBool(false)); def = this->add("dithering_local_z_direct_multicolor", coBool); def->label = L("Use direct multicolor Local-Z solver"); diff --git a/src/slic3r/GUI/MixedFilamentDialog.cpp b/src/slic3r/GUI/MixedFilamentDialog.cpp index 106ceb7bfca..7796eb71119 100644 --- a/src/slic3r/GUI/MixedFilamentDialog.cpp +++ b/src/slic3r/GUI/MixedFilamentDialog.cpp @@ -384,7 +384,7 @@ void MixedFilamentDialog::build_ui() m_match_input_card->SetBorderColorNormal(wxColour("#F0F0F0")); auto* card1_sizer = new wxBoxSizer(wxVERTICAL); - auto* filament_label = new wxStaticText(m_match_input_card, wxID_ANY, _L("Filaments:")); + auto* filament_label = new wxStaticText(m_match_input_card, wxID_ANY, _L("Filaments")); filament_label->SetFont(Label::Body_14); filament_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#242424"))); filament_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#FFFFFF"))); @@ -411,7 +411,7 @@ void MixedFilamentDialog::build_ui() card1_sizer->Add(m_match_badges_panel, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(16)); // Target Color label - auto* target_label = new wxStaticText(m_match_input_card, wxID_ANY, _L("Target Color:")); + auto* target_label = new wxStaticText(m_match_input_card, wxID_ANY, _L("Target Color")); target_label->SetFont(Label::Body_14); target_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#242424"))); target_label->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#FFFFFF"))); @@ -965,7 +965,7 @@ void MixedFilamentDialog::build_ui() m_cycle_card_sizer = new wxBoxSizer(wxVERTICAL); // Title - auto* cycle_title = new wxStaticText(m_cycle_card, wxID_ANY, _L("Pattern")); + auto* cycle_title = new wxStaticText(m_cycle_card, wxID_ANY, _L("Filaments")); cycle_title->SetFont(Label::Body_14); cycle_title->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#242424"))); cycle_title->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#FFFFFF"))); @@ -2230,9 +2230,10 @@ void MixedFilamentDialog::display_warning(const wxString& msg) if (!m_warning_panel || !m_warning_text || !m_error_panel) return; m_error_panel->Hide(); - m_warning_text->SetLabel(msg); m_warning_panel->Show(); - Layout(); + Layout(); // land panel width first, so the auto-wrap label's CalcMin gets a real width + m_warning_text->SetLabel(msg); + Layout(); // re-wrap with correct width; single Layout on macOS may calc height from stale zero-width } void MixedFilamentDialog::set_error(const wxString& msg) @@ -2240,10 +2241,11 @@ void MixedFilamentDialog::set_error(const wxString& msg) if (!m_error_panel || !m_error_text || !m_warning_panel) return; m_warning_panel->Hide(); - m_error_text->SetLabel(msg); m_error_panel->Show(); + Layout(); // land panel width first, so the auto-wrap label's CalcMin gets a real width + m_error_text->SetLabel(msg); if (m_btn_confirm) m_btn_confirm->Disable(); - Layout(); + Layout(); // re-wrap with correct width; single Layout on macOS may calc height from stale zero-width } wxString MixedFilamentDialog::get_ratio_warning_msg() @@ -3080,6 +3082,7 @@ void MixedFilamentDialog::rebuild_cycle_legend() if (m_scrolled_content) { m_scrolled_content->Layout(); m_scrolled_content->FitInside(); + m_scrolled_content->Refresh(); } } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 9c295db9e0b..994625229d5 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1528,17 +1528,37 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value) if(opt_key == "purge_in_prime_tower") wxGetApp().get_tab(Preset::TYPE_PRINT)->update(); - if (m_type == Preset::TYPE_PRINT && - opt_key == "dithering_local_z_mode" && - boost::any_cast(value) && - m_config->has("dithering_local_z_infill") && - !m_config->opt_bool("dithering_local_z_infill")) { + if (m_type == Preset::TYPE_PRINT && opt_key == "dithering_local_z_mode") { + const bool local_z_enabled = boost::any_cast(value); DynamicPrintConfig new_conf = *m_config; - new_conf.set_key_value("dithering_local_z_infill", new ConfigOptionBool(true)); - m_config_manipulation.apply(m_config, &new_conf); - + bool dependent_config_changed = false; DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config; - project_cfg.set_key_value("dithering_local_z_infill", new ConfigOptionBool(true)); + + auto set_print_bool = [this, &new_conf, &dependent_config_changed](const char *key, bool enabled) { + if (!m_config->has(key) || m_config->option(key) == nullptr || m_config->opt_bool(key) == enabled) + return; + new_conf.set_key_value(key, new ConfigOptionBool(enabled)); + dependent_config_changed = true; + }; + auto set_project_bool = [&project_cfg](const char *key, bool enabled) { + project_cfg.set_key_value(key, new ConfigOptionBool(enabled)); + }; + + if (local_z_enabled) { + set_print_bool("dithering_local_z_infill", true); + } else { + set_print_bool("dithering_local_z_whole_objects", false); + set_print_bool("dithering_local_z_infill", false); + set_project_bool("dithering_local_z_direct_multicolor", false); + } + + if (dependent_config_changed) + m_config_manipulation.apply(m_config, &new_conf); + + if (new_conf.has("dithering_local_z_whole_objects")) + set_project_bool("dithering_local_z_whole_objects", new_conf.opt_bool("dithering_local_z_whole_objects")); + if (new_conf.has("dithering_local_z_infill")) + set_project_bool("dithering_local_z_infill", new_conf.opt_bool("dithering_local_z_infill")); } if (opt_key == "enable_prime_tower") { @@ -2877,6 +2897,18 @@ static DynamicPrintConfig resolved_model_config_for_tab(const DynamicPrintConfig return resolved; } +static void sync_plate_bed_type_to_global(DynamicPrintConfig& config) +{ + if (wxGetApp().preset_bundle == nullptr) + return; + + DynamicConfig& global_cfg = wxGetApp().preset_bundle->project_config; + if (global_cfg.has("curr_bed_type")) { + BedType global_bed_type = global_cfg.opt_enum("curr_bed_type"); + config.set_key_value("curr_bed_type", new ConfigOptionEnum(global_bed_type)); + } +} + TabPrintModel::TabPrintModel(ParamsPanel* parent, std::vector const & keys) : TabPrint(parent, Preset::TYPE_MODEL) , m_keys(intersect(Preset::print_options(), keys)) @@ -2941,6 +2973,9 @@ void TabPrintModel::update_model_config() m_config->apply(*m_parent_tab->m_config); if (m_type != Preset::TYPE_PLATE) { m_config->apply_only(*wxGetApp().plate_tab->get_config(), plate_keys); + } else { + sync_plate_bed_type_to_global(m_prints.get_selected_preset().config); + sync_plate_bed_type_to_global(*m_config); } m_null_keys.clear(); if (!m_object_configs.empty()) { @@ -3134,10 +3169,9 @@ void TabPrintPlate::build() load_initial_data(); m_config->option("curr_bed_type", true); - if (m_preset_bundle->project_config.has("curr_bed_type")) { - BedType global_bed_type = m_preset_bundle->project_config.opt_enum("curr_bed_type"); - m_config->set_key_value("curr_bed_type", new ConfigOptionEnum(global_bed_type)); - } + m_prints.get_selected_preset().config.option("curr_bed_type", true); + sync_plate_bed_type_to_global(m_prints.get_selected_preset().config); + sync_plate_bed_type_to_global(*m_config); m_config->option("first_layer_sequence_choice", true); m_config->option("first_layer_print_sequence", true); m_config->option("other_layers_print_sequence", true); diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp index dd352b0fc04..474bafef73c 100644 --- a/tests/libslic3r/test_mixed_filament.cpp +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -5,6 +5,7 @@ #include "libslic3r/Print.hpp" #include "libslic3r/GCode/ToolOrdering.hpp" +#include #include #include @@ -2790,3 +2791,30 @@ TEST_CASE("dithering_local_z_mode dirty tracking via is_dirty", "[MixedFilament] edited.config.set_key_value("dithering_local_z_mode", new ConfigOptionBool(false)); CHECK(PresetCollection::is_dirty(&edited, &reference)); } + +TEST_CASE("Local Z whole object setting is available for 3MF project config", "[MixedFilament][Config]") +{ + const auto &print_options = Preset::print_options(); + CHECK(std::find(print_options.begin(), print_options.end(), "dithering_local_z_whole_objects") != print_options.end()); + + PresetBundle bundle; + REQUIRE(bundle.project_config.has("dithering_local_z_whole_objects")); + + bundle.project_config.set_key_value("dithering_local_z_whole_objects", new ConfigOptionBool(true)); + DynamicPrintConfig full_config = bundle.full_fff_config(); + REQUIRE(full_config.has("dithering_local_z_whole_objects")); + CHECK(full_config.opt_bool("dithering_local_z_whole_objects")); +} + +TEST_CASE("Local Z infill subdivision defaults inactive when Subdivide Mix Layer is off", "[MixedFilament][Config]") +{ + PresetBundle bundle; + REQUIRE(bundle.project_config.has("dithering_local_z_mode")); + REQUIRE(bundle.project_config.has("dithering_local_z_infill")); + CHECK_FALSE(bundle.project_config.opt_bool("dithering_local_z_mode")); + CHECK_FALSE(bundle.project_config.opt_bool("dithering_local_z_infill")); + + DynamicPrintConfig full_config = bundle.full_fff_config(); + REQUIRE(full_config.has("dithering_local_z_infill")); + CHECK_FALSE(full_config.opt_bool("dithering_local_z_infill")); +} From df35d8efeaabb38939d97dc0836b551d2537df9f Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 16:35:30 +0800 Subject: [PATCH 25/30] Fix filament preset validation: linear scan fallback + suffix heuristic + vendor loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - load_system_presets: only load OrcaFilamentLibrary + Snapmaker vendors to prevent other vendors from clearing incompatible Snapmaker presets - find_in_system: add linear scan fallback when binary search fails (known PresetCollection ordering issue with Snapmaker presets) - validate_filament_official Case 1: non-official system presets now walk inherits chain instead of immediately reporting unsupported vendor - Add suffix-stripping heuristic: strip OrcaSlicer copy suffix (e.g. ' - 拷贝') from filament_settings_id to match base system preset --- src/orca-slice-engine/SliceEngine.cpp | 79 ++++++++++++++++++++------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index a6c98135873..8264afbd634 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -455,14 +455,19 @@ void SliceEngine::load_system_presets() // needed — duplicate detection is not critical for cloud validation). const auto rule = ForwardCompatibilitySubstitutionRule::EnableSilent; - for (size_t i = 0; i < vendor_names.size(); ++i) { - const std::string& vendor = vendor_names[i]; - // First vendor: no base_bundle. Subsequent: pass this bundle for - // cross-vendor preset inheritance resolution. - const PresetBundle* base = (i == 0) ? nullptr : m_preset_bundle.get(); - m_preset_bundle->load_vendor_configs_from_json( - profiles_dir.string(), vendor, - PresetBundle::LoadSystem, rule, base); + // Only load Snapmaker + OrcaFilamentLibrary — the two vendors + // whose presets are relevant for cloud engine validation. + // Loading additional vendors clears Snapmaker presets that are + // incompatible with non-Snapmaker printers. + { + // Load OrcaFilamentLibrary first (generic filament base), + // then Snapmaker on top with inheritance from Orca. + const std::string vendors[] = { PresetBundle::ORCA_FILAMENT_LIBRARY, "Snapmaker" }; + for (const auto& vendor : vendors) { + m_preset_bundle->load_vendor_configs_from_json( + profiles_dir.string(), vendor, + PresetBundle::LoadSystem, rule, nullptr); + } } m_presets_available = true; @@ -684,10 +689,16 @@ bool SliceEngine::validate_filament_official(bool enforce) return false; }; - // Look up a preset name: system presets first, then project embedded + // Look up a preset name: system presets first, then project embedded. + // When find_preset's binary search fails (known ordering issue with + // Snapmaker presets), fall back to linear scan of all loaded presets. auto find_in_system = [this](const std::string& name) -> Preset* { auto* p = m_preset_bundle->filaments.find_preset(name, false); if (p && p->name == name) return p; + // Binary search failed — linear scan fallback + for (auto& preset : m_preset_bundle->filaments) { + if (preset.name == name) return &preset; + } return nullptr; }; @@ -703,23 +714,26 @@ bool SliceEngine::validate_filament_official(bool enforce) const std::string& name = filament_ids->values[i]; // Case 1: Direct system preset match - if (Preset* sys = find_in_system(name)) { - if (is_official_preset(*sys)) { - continue; // OK - } - std::string msg = "Filament \"" + name + "\" belongs to unsupported vendor"; - report(/*is_official_violation=*/true, "FILAMENT_UNSUPPORTED_VENDOR", msg); - continue; + Preset* sys = find_in_system(name); + if (sys && is_official_preset(*sys)) { + continue; // OK — directly matches an official system preset } // Case 1b: File-based check — preset exists on disk but not // found via find_preset (known issue with binary search ordering) - if (is_official_filament_file(name)) { + if (!sys && is_official_filament_file(name)) { continue; // OK } - // Case 2: Not a direct system match — walk the inheritance chain - Preset* current = find_in_project(name); + // Case 2: Try project-embedded presets. + // When sys is non-null but not official (e.g., a project-embedded + // preset loaded into the bundle via load_project_embedded_presets), + // use it as the starting point for the inherits-chain walk. + Preset* current = sys; // may be a non-official system match + if (!current) { + current = find_in_project(name); + } + if (!current) { // When not enforcing, filament config values may be embedded // directly in project_settings.config without a named preset. @@ -731,6 +745,33 @@ bool SliceEngine::validate_filament_official(bool enforce) m_stats.issues.push_back(make_warning(-1, "FILAMENT_CUSTOM_INLINE", msg)); continue; } + + // Heuristic: OrcaSlicer copies user-modified system presets as + // "SystemName - suffix" (e.g. "Generic PETG @U1 0.6 nozzle - 拷贝"). + // When the 3MF lacks an embedded preset file (user saved as a local + // user preset but not to the project), try stripping the suffix and + // matching the base name against system presets. + { + auto pos = name.rfind(" - "); + if (pos != std::string::npos && pos > 0) { + std::string base_name = name.substr(0, pos); + // Try system preset lookup first + Preset* base_preset = find_in_system(base_name); + if (!base_preset && is_official_filament_file(base_name)) { + // File exists but not loaded (shouldn't happen after + // linear fallback, but kept as safety net) + continue; // can't substitute without config + } + if (base_preset) { + BOOST_LOG_TRIVIAL(info) << "Filament \"" << name + << "\" resolved via suffix-stripping heuristic to \"" + << base_name << "\""; + substitute_filament_params(filament_ids, i, *base_preset, name); + continue; + } + } + } + std::string msg = "Filament \"" + name + "\" is not a recognized preset"; // FILAMENT_UNKNOWN: preset truly doesn't exist — always an error report(/*is_official_violation=*/false, "FILAMENT_UNKNOWN", msg); From bd14f61bdd859eaef20cac45f261d96103f2d556 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 16:47:05 +0800 Subject: [PATCH 26/30] Replace suffix-stripping heuristic with longest-prefix matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of trying to parse suffix patterns (' - 拷贝', ' N'), iterate all loaded official presets and find the one that is the longest prefix of the unknown filament name. This handles all OrcaSlicer/Bambu copy naming conventions uniformly. --- src/orca-slice-engine/SliceEngine.cpp | 40 ++++++++++++--------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 8264afbd634..c617f7fb7c5 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -746,29 +746,25 @@ bool SliceEngine::validate_filament_official(bool enforce) continue; } - // Heuristic: OrcaSlicer copies user-modified system presets as - // "SystemName - suffix" (e.g. "Generic PETG @U1 0.6 nozzle - 拷贝"). - // When the 3MF lacks an embedded preset file (user saved as a local - // user preset but not to the project), try stripping the suffix and - // matching the base name against system presets. + // Heuristic: user-modified system presets are exported with + // a suffix (e.g. "Generic PETG @U1 0.6 nozzle - 拷贝" or + // "Generic PETG @U1 0.6 nozzle 1"). Match by prefix: iterate + // all loaded official presets and find the longest matching one. { - auto pos = name.rfind(" - "); - if (pos != std::string::npos && pos > 0) { - std::string base_name = name.substr(0, pos); - // Try system preset lookup first - Preset* base_preset = find_in_system(base_name); - if (!base_preset && is_official_filament_file(base_name)) { - // File exists but not loaded (shouldn't happen after - // linear fallback, but kept as safety net) - continue; // can't substitute without config - } - if (base_preset) { - BOOST_LOG_TRIVIAL(info) << "Filament \"" << name - << "\" resolved via suffix-stripping heuristic to \"" - << base_name << "\""; - substitute_filament_params(filament_ids, i, *base_preset, name); - continue; - } + Preset* best = nullptr; + for (auto& preset : m_preset_bundle->filaments) { + if (!is_official_preset(preset)) continue; + if (preset.name.size() >= name.size()) continue; + if (name.compare(0, preset.name.size(), preset.name) != 0) continue; + if (!best || preset.name.size() > best->name.size()) + best = &preset; + } + if (best) { + BOOST_LOG_TRIVIAL(info) << "Filament \"" << name + << "\" resolved via prefix matching to \"" + << best->name << "\""; + substitute_filament_params(filament_ids, i, *best, name); + continue; } } From fce739af356729b7a0f192e5cf8e7fc719b5ce9c Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 16:51:16 +0800 Subject: [PATCH 27/30] Improve FILAMENT_SUBSTITUTED warning message clarity - When names differ: 'Custom filament X replaced with official preset Y for cloud safety' - When names are the same (prefix match): 'config values updated from official preset' --- src/orca-slice-engine/SliceEngine.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index c617f7fb7c5..67aca894c84 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -918,9 +918,13 @@ void SliceEngine::substitute_filament_params(ConfigOptionStrings* filament_ids, dst_vec->set_at(src_vec, dst_idx, 0); } - m_stats.issues.push_back(make_warning(-1, "FILAMENT_SUBSTITUTED", - std::string("Filament \"") + original_name - + "\" substituted with official preset \"" + official_parent.name + "\"")); + const std::string msg = original_name == official_parent.name + ? std::string("Filament \"") + original_name + + "\" config values updated from official preset" + : std::string("Custom filament \"") + original_name + + "\" replaced with official preset \"" + + official_parent.name + "\" for cloud safety"; + m_stats.issues.push_back(make_warning(-1, "FILAMENT_SUBSTITUTED", msg)); } bool SliceEngine::validate_printer_model() From 7566d894fd118ff5a657644bcb8a9a6b1b178b8e Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 16:53:47 +0800 Subject: [PATCH 28/30] Improve clarity of engine warning/error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRINTER_SUBSTITUTED: match FILAMENT_SUBSTITUTED format — show 'replaced with X for cloud safety' or 'config values updated' depending on whether names differ. PRESET_MODIFIED_GCODES: 'Modified G-code keys found' → 'Custom G-code detected (X) — disabled for cloud safety'. FILAMENT_CUSTOM_INLINE: remove jargon ('inline', 'per-extruder config values'), show 'custom filament without a preset definition — accepted in allow-custom mode'. --- src/orca-slice-engine/SliceEngine.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index 67aca894c84..bc074059981 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -549,9 +549,8 @@ void SliceEngine::validate_presets() std::string details; for (const auto& name : modified_gcodes) details += (details.empty() ? "" : ", ") + name; - std::string msg = "Modified G-code keys found in presets"; - if (!details.empty()) - msg += ": " + details; + std::string msg = "Custom G-code detected in presets (" + details + + ") — disabled for cloud safety"; m_stats.issues.push_back(make_warning(-1, "PRESET_MODIFIED_GCODES", msg)); break; } @@ -739,8 +738,8 @@ bool SliceEngine::validate_filament_official(bool enforce) // directly in project_settings.config without a named preset. if (!enforce && has_inline_filament_config(i)) { std::string msg = "Filament \"" + name - + "\" is an inline custom filament (no preset definition, " - + "but per-extruder config values are present)"; + + "\" is a custom filament without a preset definition" + + " — accepted in allow-custom mode"; BOOST_LOG_TRIVIAL(warning) << msg; m_stats.issues.push_back(make_warning(-1, "FILAMENT_CUSTOM_INLINE", msg)); continue; @@ -1098,9 +1097,13 @@ void SliceEngine::substitute_printer_params(const std::string& original_name, << ": " << e.what(); } - m_stats.issues.push_back(make_warning(-1, "PRINTER_SUBSTITUTED", - std::string("Printer preset \"") + original_name - + "\" substituted with official preset \"" + parent_name + "\"")); + const std::string printer_msg = original_name == parent_name + ? std::string("Printer preset \"") + original_name + + "\" config values updated from official preset" + : std::string("Custom printer preset \"") + original_name + + "\" replaced with official preset \"" + + parent_name + "\" for cloud safety"; + m_stats.issues.push_back(make_warning(-1, "PRINTER_SUBSTITUTED", printer_msg)); } // ============================================================================ From d545199c123b55d2347207d970d6e133ae027959 Mon Sep 17 00:00:00 2001 From: joyx_desktop Date: Fri, 29 May 2026 17:50:38 +0800 Subject: [PATCH 29/30] Add prefix matching heuristic for printer presets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user-modified printer preset (e.g. 'Snapmaker U1 (0.6 nozzle) - 拷贝') is exported in a 3MF and the cloud engine cannot find it, iterate all loaded official printer presets and match by longest prefix. Mirrors the same logic already applied to filament presets. --- src/orca-slice-engine/SliceEngine.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/orca-slice-engine/SliceEngine.cpp b/src/orca-slice-engine/SliceEngine.cpp index bc074059981..cf718854608 100644 --- a/src/orca-slice-engine/SliceEngine.cpp +++ b/src/orca-slice-engine/SliceEngine.cpp @@ -986,6 +986,27 @@ bool SliceEngine::validate_printer_official(bool enforce) } if (!current) { + // Prefix matching heuristic: user-modified system printer presets + // are exported with a suffix (e.g. "Snapmaker U1 (0.6 nozzle) - 拷贝"). + // Match the longest official printer preset that is a prefix. + if (enforce && m_preset_bundle) { + Preset* best = nullptr; + for (auto& preset : m_preset_bundle->printers) { + if (!is_official_machine_file(preset.name)) continue; + if (preset.name.size() >= name.size()) continue; + if (name.compare(0, preset.name.size(), preset.name) != 0) continue; + if (!best || preset.name.size() > best->name.size()) + best = &preset; + } + if (best) { + BOOST_LOG_TRIVIAL(info) << "Printer \"" << name + << "\" resolved via prefix matching to \"" + << best->name << "\""; + substitute_printer_params(name, best->name); + return true; + } + } + if (!enforce) { BOOST_LOG_TRIVIAL(warning) << "Printer preset \"" << name << "\" not found in system presets; accepted in allow-custom mode"; From 9921461fc99745ac22ef331c7720383aabc04987 Mon Sep 17 00:00:00 2001 From: zhangzhend0ng Date: Sun, 31 May 2026 23:33:16 +0800 Subject: [PATCH 30/30] Fix painted region rebuild after mixed filament edits (#408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix 3MF persistence for Local-Z project settings Fixes #125. Add Local-Z Full Domain and related mixed-filament project settings to the project config and import allow-lists so they survive 3MF save/load and model import. * Load Local-Z settings from imported 3MF geometry Fix geometry-only 3MF import so saved Local-Z project settings override the current unsaved state even when importing into a non-empty scene. Mirror the print-tab Local-Z booleans as well so the UI does not drift back to the old preset values. * Keep Local-Z infill option in sync with subdivision mode Default Apply subdivision to infill to off while Subdivide Mix Layer is disabled, auto-enable it when subdivision is enabled, and clear dependent Local-Z child settings when subdivision is disabled so saved project config does not retain stale active child values. * bugfix: mix dialog error/warning panel oversized on first display - Reorder Show+Layout before SetLabel so hidden LB_AUTO_WRAP Label gets valid width for wrapping, preventing NSTextField from reporting overly wide intrinsicContentSize - Clean up i18n keys: drop colons (Filaments:/Target Color:), fix cycle card title Pattern→Filaments - Sync zh_CN translations * Refresh print tab after importing Local-Z settings After geometry-only 3MF import, reload the print tab when Local-Z settings were imported so the visible Subdivide Mix Layer controls match the project config used for slicing. * Do not import Local-Z process settings with geometry File -> Import 3MF should preserve the current project's process parameter state. Stop applying saved Local-Z project/print settings during geometry-only 3MF import; those settings are still loaded when opening a project with config. * Fix inherited plate bed type dirty state * i18n: add zh_CN translation for "Apply subdivision to infill" * docs: add inline comments explaining double-Layout macOS workaround in MixedFilamentDialog The error/warning labels use LB_AUTO_WRAP, which requires two Layout() passes on macOS: the first lands the panel's actual width after Show(), the second re-wraps the label text at the correct width. Without this, a single Layout can query a stale zero-width from CalcMin and produce an oversized dialog on first display. * Fix painted region rebuild after mixed filament edits * same layer pointillism comment --------- Co-authored-by: ratdoux Co-authored-by: ZhangZheng <67276816+LuckZAE@users.noreply.github.com> --- src/libslic3r/PrintApply.cpp | 40 ++++++++++ tests/libslic3r/test_mixed_filament.cpp | 102 +++++++++++++++++++++++- 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintApply.cpp b/src/libslic3r/PrintApply.cpp index ae0fc36d17a..868df798c37 100644 --- a/src/libslic3r/PrintApply.cpp +++ b/src/libslic3r/PrintApply.cpp @@ -1177,8 +1177,44 @@ static void append_mixed_component_extruders(const MixedFilamentManager &mixed_m } } +static bool painted_region_targets_match(const PrintObjectRegions &print_object_regions, + const std::vector &painting_extruders) +{ + std::vector expected_extruders = painting_extruders; + std::sort(expected_extruders.begin(), expected_extruders.end()); + expected_extruders.erase(std::unique(expected_extruders.begin(), expected_extruders.end()), expected_extruders.end()); + + for (const PrintObjectRegions::LayerRangeRegions &layer_range : print_object_regions.layer_ranges) { + std::vector> expected_targets; + expected_targets.reserve(layer_range.volume_regions.size() * expected_extruders.size()); + + for (int parent_region_id = 0; parent_region_id < int(layer_range.volume_regions.size()); ++parent_region_id) { + const PrintObjectRegions::VolumeRegion &parent_region = layer_range.volume_regions[parent_region_id]; + if (parent_region.region != nullptr && + (parent_region.model_volume->is_model_part() || parent_region.model_volume->is_modifier()) && + mm_paint_applies_to_parent_region(layer_range, parent_region_id)) { + for (unsigned int extruder_id : expected_extruders) + expected_targets.emplace_back(parent_region_id, extruder_id); + } + } + + std::vector> actual_targets; + actual_targets.reserve(layer_range.painted_regions.size()); + for (const PrintObjectRegions::PaintedRegion &painted_region : layer_range.painted_regions) + actual_targets.emplace_back(painted_region.parent, painted_region.extruder_id); + + std::sort(expected_targets.begin(), expected_targets.end()); + std::sort(actual_targets.begin(), actual_targets.end()); + if (actual_targets != expected_targets) + return false; + } + + return true; +} + static bool same_layer_pointillism_enabled(const MixedFilamentManager &mixed_mgr) { + // Deprecated: same-layer pointillism is disabled and will be removed. #if 0 for (const MixedFilament &mf : mixed_mgr.mixed_filaments()) if (mf.enabled && mf.distribution_mode == int(MixedFilament::SameLayerPointillisme)) @@ -1897,6 +1933,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_ print_object_regions->clear(); model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::Invalid; print_regions_reshuffled = true; + } else if (print_object_regions && !painted_region_targets_match(*print_object_regions, painting_extruders)) { + invalidate(); + model_object_status.print_object_regions_status = ModelObjectStatus::PrintObjectRegionsStatus::PartiallyValid; + print_regions_reshuffled = true; } else if (print_object_regions && verify_update_print_object_regions( print_object.model_object()->volumes, diff --git a/tests/libslic3r/test_mixed_filament.cpp b/tests/libslic3r/test_mixed_filament.cpp index 474bafef73c..0fc3d937658 100644 --- a/tests/libslic3r/test_mixed_filament.cpp +++ b/tests/libslic3r/test_mixed_filament.cpp @@ -4,6 +4,8 @@ #include "libslic3r/PresetBundle.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/GCode/ToolOrdering.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/TriangleSelector.hpp" #include #include @@ -49,6 +51,57 @@ static unsigned int virtual_id_for_stable_id(const std::vector &m return 0; } +static std::string single_custom_mixed_definition(unsigned int component_a, unsigned int component_b, uint64_t stable_id) +{ + const std::vector colors = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00"}; + MixedFilamentManager mgr; + mgr.add_custom_filament(component_a, component_b, 50, colors); + + MixedFilament &row = mgr.mixed_filaments().front(); + row.stable_id = stable_id; + row.distribution_mode = int(MixedFilament::Simple); + row.manual_pattern.clear(); + + return mgr.serialize_custom_entries(); +} + +static DynamicPrintConfig mixed_region_print_config(const std::string &mixed_definitions) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_num_extruders(4); + config.set_num_filaments(4); + // Print::apply uses filament_diameter.size() as the physical filament count. + config.option("filament_diameter")->values = {1.75, 1.76, 1.77, 1.78}; + config.option("filament_colour")->values = {"#FF0000", "#00FF00", "#0000FF", "#FFFF00"}; + config.set("mixed_filament_definitions", mixed_definitions); + return config; +} + +static std::vector first_layer_range_painted_extruders(const Print &print) +{ + std::vector extruders; + if (print.objects().empty()) + return extruders; + + const PrintObjectRegions *regions = print.objects().front()->shared_regions(); + if (regions == nullptr || regions->layer_ranges.empty()) + return extruders; + + for (const PrintObjectRegions::PaintedRegion &painted_region : regions->layer_ranges.front().painted_regions) + extruders.emplace_back(painted_region.extruder_id); + + std::sort(extruders.begin(), extruders.end()); + extruders.erase(std::unique(extruders.begin(), extruders.end()), extruders.end()); + return extruders; +} + +static const PrintObjectRegions *first_print_object_regions(const Print &print) +{ + if (print.objects().empty()) + return nullptr; + return print.objects().front()->shared_regions(); +} + struct MixedAutoGenerateGuard { explicit MixedAutoGenerateGuard(bool enabled) @@ -504,6 +557,49 @@ TEST_CASE("Mixed filament painted-region resolver preserves virtual channels for CHECK_THAT(double(mgr.component_surface_offset(3, 2, 0)), WithinAbs(0.0, 0.0001)); } +TEST_CASE("Mixed filament component edits rebuild painted region targets", "[MixedFilament][PrintApply]") +{ + MixedAutoGenerateGuard guard(false); + + Model model; + ModelObject *object = model.add_object(); + object->name = "mixed-painted-object.stl"; + ModelVolume *volume = object->add_volume(make_cube(20., 20., 20.)); + object->add_instance(); + object->ensure_on_bed(); + + constexpr unsigned int mixed_virtual_id = 5; + TriangleSelector selector(volume->mesh()); + selector.set_facet(0, EnforcerBlockerType(mixed_virtual_id)); + REQUIRE(volume->mmu_segmentation_facets.set(selector)); + + constexpr uint64_t stable_id = 4242; + DynamicPrintConfig config = mixed_region_print_config(single_custom_mixed_definition(1, 2, stable_id)); + + Print print; + print.set_status_silent(); + print.apply(model, config); + REQUIRE(print.objects().size() == 1); + const PrintObjectRegions *initial_regions = first_print_object_regions(print); + REQUIRE(initial_regions != nullptr); + REQUIRE_FALSE(initial_regions->layer_ranges.empty()); + const std::vector expected_initial_extruders {1, 2, mixed_virtual_id}; + CHECK(first_layer_range_painted_extruders(print) == expected_initial_extruders); + + config.set("mixed_filament_definitions", single_custom_mixed_definition(3, 2, stable_id)); + print.apply(model, config); + const PrintObjectRegions *updated_regions = first_print_object_regions(print); + REQUIRE(updated_regions != nullptr); + REQUIRE_FALSE(updated_regions->layer_ranges.empty()); + const std::vector expected_updated_extruders {2, 3, mixed_virtual_id}; + CHECK(first_layer_range_painted_extruders(print) == expected_updated_extruders); + + config.set("mixed_filament_definitions", single_custom_mixed_definition(3, 4, stable_id)); + print.apply(model, config); + const std::vector expected_updated_second_component_extruders {3, 4, mixed_virtual_id}; + CHECK(first_layer_range_painted_extruders(print) == expected_updated_second_component_extruders); +} + TEST_CASE("ExtrusionPath copies preserve inset index", "[MixedFilament]") { ExtrusionPath src(erPerimeter); @@ -2801,7 +2897,8 @@ TEST_CASE("Local Z whole object setting is available for 3MF project config", "[ REQUIRE(bundle.project_config.has("dithering_local_z_whole_objects")); bundle.project_config.set_key_value("dithering_local_z_whole_objects", new ConfigOptionBool(true)); - DynamicPrintConfig full_config = bundle.full_fff_config(); + DynamicPrintConfig full_config = DynamicPrintConfig::full_print_config(); + full_config.apply(bundle.project_config); REQUIRE(full_config.has("dithering_local_z_whole_objects")); CHECK(full_config.opt_bool("dithering_local_z_whole_objects")); } @@ -2814,7 +2911,8 @@ TEST_CASE("Local Z infill subdivision defaults inactive when Subdivide Mix Layer CHECK_FALSE(bundle.project_config.opt_bool("dithering_local_z_mode")); CHECK_FALSE(bundle.project_config.opt_bool("dithering_local_z_infill")); - DynamicPrintConfig full_config = bundle.full_fff_config(); + DynamicPrintConfig full_config = DynamicPrintConfig::full_print_config(); + full_config.apply(bundle.project_config); REQUIRE(full_config.has("dithering_local_z_infill")); CHECK_FALSE(full_config.opt_bool("dithering_local_z_infill")); }