From a80d0b49f0c0a5cebf9bcbd08d42d396e9be3cd2 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Tue, 22 Sep 2026 18:17:16 +0800 Subject: [PATCH] Report Per-Object Slicing Errors in the CLI G-code generation collects errors raised per object, such as an empty first layer, into one SlicingErrors exception whose own message is just "Errors". The CLI's generic handler printed that word and recorded the generic slicing error text, so a headless caller had nothing to act on. Let Print render the per-object messages with each object's name, and have the CLI catch SlicingErrors ahead of the generic handler, print that text and record it as the result's error string. The exit code is unchanged. A unit test lifts a cube off the bed and checks the message names the object. --- src/OrcaSlicer.cpp | 6 ++++++ src/libslic3r/Print.cpp | 19 +++++++++++++++++++ src/libslic3r/Print.hpp | 4 ++++ tests/fff_print/test_print.cpp | 26 ++++++++++++++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 0c9bd0b1ee..7f65ba0fb9 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -7015,6 +7015,12 @@ int CLI::run(int argc, char **argv) } } sliced_info.sliced_plates.push_back(sliced_plate_info); + } catch (const Slic3r::SlicingErrors &exs) { + const std::string message = print_fff ? print_fff->slicing_errors_message(exs) : std::string(exs.what()); + BOOST_LOG_TRIVIAL(error) << "found slicing or export error for partplate " << index+1 << ": " << message; + boost::nowide::cerr << message << std::endl; + record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, message, sliced_info); + flush_and_exit(CLI_SLICING_ERROR); } catch (const std::exception &ex) { BOOST_LOG_TRIVIAL(error) << "found slicing or export error for partplate "<id().id == error.objectId()) { + object_name = object->model_object()->name; + break; + } + if (!message.empty()) + message += "\n"; + message += object_name.empty() ? std::string(error.what()) : object_name + ": " + error.what(); + } + return message; +} + StringObjectException Print::validate(std::vector *warnings, Polygons* collison_polygons, std::vector>* height_polygons) const { auto add_warning = [warnings](StringObjectException w) { diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 9c1782e047..12be35ea3c 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -30,6 +30,8 @@ namespace Slic3r { +class SlicingErrors; + class GCode; class Layer; class ModelObject; @@ -967,6 +969,8 @@ public: // Returns an empty string if valid, otherwise returns an error message. StringObjectException validate(std::vector *warnings = nullptr, Polygons* collison_polygons = nullptr, std::vector>* height_polygons = nullptr) const override; + // The per-object messages of a SlicingErrors, each prefixed with its object's name. + std::string slicing_errors_message(const SlicingErrors &errors) const; double skirt_first_layer_height() const; Flow brim_flow() const; Flow skirt_flow() const; diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index d7cd965343..383a7165d5 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -505,3 +505,29 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos); } } + +TEST_CASE("Slicing errors are reported per object with the object's name", "[Print]") +{ + Print print; + Model model; + init_print({Slic3r::Test::cube(20.)}, print, model); + // Lift the cube off the bed: its first layer is empty, which G-code export reports per object. + ModelObject *object = model.objects.front(); + object->name = "floating cube"; + object->instances.front()->set_offset(object->instances.front()->get_offset() + Vec3d(0., 0., 2.)); + print.apply(model, DynamicPrintConfig::full_print_config()); + print.set_status_silent(); + + ScopedTemporaryFile temp(".gcode"); + std::string message; + try { + print.process(); + print.export_gcode(temp.string(), nullptr, nullptr); + FAIL("slicing did not report the empty first layer"); + } catch (const SlicingErrors &errors) { + REQUIRE(errors.errors_.size() == 1); + message = print.slicing_errors_message(errors); + } + CHECK(message.rfind("floating cube: ", 0) == 0); + CHECK(message.find("empty first layer") != std::string::npos); +}