Files
OrcaSlicer/tests/fff_print/test_gcodeprocessor.cpp
Hanif Koh 7b05bc5ea1 Keep Layer Pauses on Their Own Layer in the Preview
The G-code processor remembers where a pause, color change or custom
G-code vertex sits in the move list, so it can move that vertex to the
first extrusion after it. A pause flushes the time planner first, which
inserts actual speed moves ahead of the vertex, so the remembered index
pointed at an earlier move: the previous layer's last extrusion was
moved instead, a real move was erased, and the time blocks queued after
the vertex were left one move off.

Shift the remembered index by the moves each flush inserts before it,
and shift the queued blocks after the erase. The Preview numbers layers
from the move order, so the stray move showed up as extra layers and
listed pauses on the wrong layer.
2026-10-21 13:50:40 +08:00

602 lines
27 KiB
C++

#include <catch2/catch_all.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <initializer_list>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Point.hpp"
#include <sstream>
#include <string>
#include <utility>
#include <vector>
using namespace Slic3r;
// Bambu firmware uses the " FEATURE: " style reserved tags, everything else the Slic3r-compatible
// "TYPE:" style, so which list applies depends on the printer kind passed in.
TEST_CASE("Reserved keyword detection follows the printer kind it is given", "[GCodeProcessor]")
{
struct Case
{
const char* name;
std::string gcode;
bool reserved_on_bbl;
bool reserved_on_non_bbl;
};
const auto test_case = GENERATE(values<Case>({
{"compatible feature tag", ";TYPE:Prime tower", false, true},
{"compatible layer tag", ";LAYER_CHANGE", false, true},
{"bbl feature tag", "; FEATURE: Outer wall", true, false},
{"tag shared by both lists", ";_GP_FIRST_LINE_M73_PLACEHOLDER", true, true},
{"bbl spells this one with a leading space", ";COLOR_CHANGE", false, true},
{"ordinary comment", "; heat the bed", false, false},
{"not a comment at all", "G1 X10 Y10 F3000", false, false},
// A tag counts only as the whole comment's prefix, so neither a tag mentioned mid-comment
// nor one trailing a real command is a reserved use.
{"tag text later in the comment", "; the TYPE:Prime tower marker", false, false},
{"tag trailing a command", "G1 X10 ;TYPE:Prime tower", false, false},
}));
DYNAMIC_SECTION(test_case.name)
{
std::vector<std::string> tags;
REQUIRE(GCodeProcessor::contains_reserved_tags(test_case.gcode, 5, tags, true) == test_case.reserved_on_bbl);
tags.clear();
REQUIRE(GCodeProcessor::contains_reserved_tags(test_case.gcode, 5, tags, false) == test_case.reserved_on_non_bbl);
}
}
TEST_CASE("Reserved keyword detection reports every offending line", "[GCodeProcessor]")
{
const std::string gcode = ";TYPE:Prime tower\nG1 X10\n;LAYER_CHANGE\n";
std::vector<std::string> tags;
REQUIRE(GCodeProcessor::contains_reserved_tags(gcode, 5, tags, false));
REQUIRE(tags.size() == 2);
// Reported in the order they appear, which is what makes the max_count cut-off meaningful.
CHECK(tags[0] == "TYPE:Prime tower");
CHECK(tags[1] == "LAYER_CHANGE");
SECTION("the reported count is capped at max_count")
{
tags.clear();
REQUIRE(GCodeProcessor::contains_reserved_tags(gcode, 1, tags, false));
CHECK(tags.size() == 1);
CHECK(tags[0] == "TYPE:Prime tower");
}
SECTION("a max_count of zero still reports the first tag")
{
tags.clear();
REQUIRE(GCodeProcessor::contains_reserved_tags(gcode, 0, tags, false));
CHECK(tags.size() == 1);
}
SECTION("g-code with nothing reserved in it reports nothing")
{
tags.clear();
CHECK_FALSE(GCodeProcessor::contains_reserved_tags("G28\n; home all axes\n", 5, tags, false));
CHECK(tags.empty());
}
}
namespace {
void process_gcode(const std::string &gcode, GCodeProcessorResult &result)
{
FullPrintConfig config;
config.gcode_flavor.value = gcfMarlinFirmware;
// s_IsBBLPrinter selects the "; FEATURE: " role tags the G-code uses.
const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter;
const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; });
GCodeProcessor::s_IsBBLPrinter = true;
ScopedTemporaryFile temp(".gcode");
std::ofstream(temp.string()) << gcode;
GCodeProcessor processor;
processor.apply_config(config);
processor.process_file(temp.string());
result = std::move(processor.extract_result());
}
// Closed outer-wall squares, each after a fast travel and before an inner-wall move, so the processor
// records seams and inserts actual speed moves. virtual_moves adds a VG1 move after each square.
void process_squares(int squares, GCodeProcessorResult &result, bool virtual_moves = false)
{
std::ostringstream gcode;
gcode << "M83\nG90\n";
for (int i = 0; i < squares; ++i) {
gcode << "G1 X10 Y10 Z" << 0.2 * (i + 1) << " F12000\n"
<< "; FEATURE: Outer wall\n"
<< "G1 X50 Y10 E2 F3000\nG1 X50 Y50 E2\nG1 X10 Y50 E2\nG1 X10 Y10 E2\n"
<< "; FEATURE: Inner wall\n"
<< "G1 X12 Y12 E0.1\nG1 X30 Y12 E1\n";
if (virtual_moves)
gcode << "VG1 X20 Y30 F12000\n";
}
process_gcode(gcode.str(), result);
}
// Objects A and B on the first layer and A again on the second, with A's brim and support. The skirt and the
// prime tower belong to neither.
void process_two_objects(GCodeProcessorResult &result)
{
std::ostringstream gcode;
gcode << "M83\nG90\n"
<< "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.2 F12000\n"
<< "; FEATURE: Skirt\nG1 X0 Y100 E5 F3000\n"
<< "; FEATURE: Brim\nG1 X8 Y8 F12000\nG1 X12 Y8 E1 F3000\n"
<< "; FEATURE: Support\nG1 X10 Y20 F12000\nG1 X10 Y30 E1 F3000\n"
<< "; FEATURE: Outer wall\nG1 X10 Y10 F12000\nG1 X20 Y10 E1 F3000\n"
<< "; FEATURE: Outer wall\nG1 X50 Y50 F12000\nG1 X60 Y50 E2 F3000\n"
<< "; FEATURE: Prime tower\nG1 X80 Y80 F12000\nG1 X90 Y80 E1 F3000\n"
<< "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\nG1 Z0.4 F12000\n"
<< "; FEATURE: Outer wall\nG1 X10 Y10 F12000\nG1 X20 Y10 E1 F3000\n";
process_gcode(gcode.str(), result);
}
// Bead centers of process_two_objects(), half the 0.2 mm layer below the nozzle.
const Vec3d a_brim(10., 8., 0.1), a_support(10., 25., 0.1), a_wall_0(15., 10., 0.1), a_wall_1(15., 10., 0.3), b_wall(55., 50., 0.1);
Vec3d center_of(const GCodeProcessorResult::ObjectMass::Sum &sum) { return sum.moment / sum.mass; }
// One filament, so each bead weighs as much as the E it was extruded with.
Vec3d weighted_center(std::initializer_list<std::pair<double, Vec3d>> beads)
{
double mass = 0.;
Vec3d moment = Vec3d::Zero();
for (const auto &[e, center] : beads) {
mass += e;
moment += e * center;
}
return moment / mass;
}
bool is_block_move(const GCodeProcessorResult::MoveVertex &move)
{
return !move.internal_only && (move.type == EMoveType::Extrude || move.type == EMoveType::Travel);
}
} // namespace
TEST_CASE("Loaded manual tool change comments switch subsequent moves when enabled", "[GCodeProcessor][Regression]")
{
const bool manual_filament_change = GENERATE(false, true);
CAPTURE(manual_filament_change);
const DynamicPrintConfig config = Test::multifilament_config(2, {
{ "gcode_flavor", "marlin" },
{ "filament_type", "PLA;PETG" },
{ "manual_filament_change", manual_filament_change },
});
const bool was_bbl_printer = GCodeProcessor::s_IsBBLPrinter;
const ScopeGuard restore_bbl_printer([was_bbl_printer] { GCodeProcessor::s_IsBBLPrinter = was_bbl_printer; });
GCodeProcessor::s_IsBBLPrinter = false;
ScopedTemporaryFile temp(".gcode");
{
std::ofstream out(temp.string());
REQUIRE(out.is_open());
out << "; generated by OrcaSlicer\n"
<< "G90\nM83\nT0\n"
<< "G1 X10 Y10 Z0.2 F3000\n"
<< "G1 X20 E1\n"
<< "; MANUAL_TOOL_CHANGE T1\n"
<< "G1 X30\nG1 X40 E1\nG1 X50\nG1 X60 E1\n";
// Let process_file load the embedded DynamicPrintConfig, as the standalone viewer does.
out << "; CONFIG_BLOCK_START\n";
for (const std::string &key : config.keys())
out << "; " << key << " = " << config.opt_serialize(key) << '\n';
out << "; CONFIG_BLOCK_END\n";
REQUIRE(out.good());
}
GCodeProcessor processor;
processor.process_file(temp.string());
const auto &result = processor.get_result();
REQUIRE(result.filaments_count == 2);
constexpr unsigned int tool_change_line = 7;
size_t before = 0;
size_t after = 0;
for (const auto &move : result.moves) {
if (!is_block_move(move))
continue;
CAPTURE(move.gcode_id, move.type);
if (move.gcode_id < tool_change_line) {
++before;
CHECK(move.extruder_id == 0);
} else if (move.gcode_id > tool_change_line) {
++after;
CHECK(move.extruder_id == (manual_filament_change ? 1 : 0));
}
}
REQUIRE(before > 0);
REQUIRE(after > 0);
}
TEST_CASE("Actual speed moves are inserted on their block's segment just before its move", "[GCodeProcessor]")
{
// 60 squares take several planner passes, which remap the blocks kept between passes.
const int squares = GENERATE(10, 60);
const bool virtual_moves = GENERATE(false, true);
GCodeProcessorResult result;
process_squares(squares, result, virtual_moves);
const auto &moves = result.moves;
constexpr size_t normal = size_t(PrintEstimatedStatistics::ETimeMode::Normal);
size_t inserted = 0;
for (size_t i = 1; i < moves.size(); ++i) {
if (!moves[i].internal_only)
continue;
++inserted;
// Inserted moves have zero time, but a VG1 block's time is written to whatever move its move_id names.
if (!virtual_moves)
CHECK(moves[i].time[normal] == 0.f);
size_t block = i + 1;
while (block < moves.size() && moves[block].internal_only)
++block;
size_t previous = i - 1;
while (previous > 0 && moves[previous].internal_only)
--previous;
REQUIRE(block < moves.size());
CHECK(moves[block].gcode_id == moves[i].gcode_id);
const Vec3f segment = moves[block].position - moves[previous].position;
const Vec3f offset = moves[i].position - moves[previous].position;
CHECK(segment.cross(offset).norm() / segment.norm() < 1e-3f);
}
REQUIRE(inserted > 0);
}
TEST_CASE("A seam takes the actual speed of the move it follows", "[GCodeProcessor]")
{
GCodeProcessorResult result;
// 10 squares fit in one planner pass, so the seam's move and the block after it are timed together.
process_squares(10, result);
const auto &moves = result.moves;
size_t seams = 0;
for (size_t i = 1; i < moves.size(); ++i)
if (moves[i].type == EMoveType::Seam && is_block_move(moves[i - 1])) {
++seams;
CHECK_THAT(moves[i].actual_feedrate, Catch::Matchers::WithinAbs(moves[i - 1].actual_feedrate, 1e-4));
}
REQUIRE(seams > 0);
}
TEST_CASE("A pause keeps the moves around it in order and on their own layers", "[GCodeProcessor][Regression]")
{
// libvgcode numbers layers from the order of the moves, so the moves around a pause must stay in layer and
// line order. The pause vertex itself moves to the first extrusion after it and keeps its own line.
std::ostringstream gcode;
gcode << "M83\nG90\n";
for (int layer = 1; layer <= 3; ++layer) {
gcode << "; CHANGE_LAYER\n; LAYER_HEIGHT: 0.2\n";
if (layer == 2)
gcode << "G1 E-0.8 F3000\n; PAUSE_PRINTING\nM601\n";
gcode << "G1 X10 Y10 F12000\nG1 Z" << 0.2 * layer << "\n";
if (layer == 2)
gcode << "G1 E0.8 F3000\n";
gcode << "; FEATURE: Outer wall\n"
<< "G1 X50 Y10 E2 F3000\nG1 X50 Y50 E2\nG1 X10 Y50 E2\nG1 X10 Y10 E2\n";
}
GCodeProcessorResult result;
process_gcode(gcode.str(), result);
const auto &moves = result.moves;
REQUIRE(result.custom_gcode_per_print_z.size() == 1);
CHECK_THAT(result.custom_gcode_per_print_z.front().print_z, Catch::Matchers::WithinAbs(0.4, 1e-4));
REQUIRE(std::count_if(moves.begin(), moves.end(), [](const auto &move) { return move.internal_only; }) > 0);
const auto pause = std::find_if(moves.begin(), moves.end(), [](const auto &move) { return move.type == EMoveType::Pause_Print; });
REQUIRE(pause != moves.end());
CHECK(pause->layer_id == 1);
CHECK_THAT(pause->position.z(), Catch::Matchers::WithinAbs(0.4f, 1e-4f));
const GCodeProcessorResult::MoveVertex *previous = nullptr;
for (const auto &move : moves) {
if (move.type == EMoveType::Pause_Print)
continue;
if (previous != nullptr) {
CAPTURE(previous->gcode_id, move.gcode_id, static_cast<int>(move.type));
CHECK(move.layer_id >= previous->layer_id);
CHECK(move.gcode_id >= previous->gcode_id);
}
previous = &move;
}
}
TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[GCodeProcessor]")
{
struct Case
{
const char* name;
bool preheat_backtrace;
bool pre_heating;
};
const auto test_case = GENERATE(values<Case>({
{ "written by size", false, false },
{ "written by time for the preheat backtrace", true, false },
{ "rewritten by the pre-heating pass", false, true },
}));
INFO(test_case.name);
DynamicPrintConfig config = Test::multifilament_config(2, {
{ "single_extruder_multi_material", 0 },
{ "ooze_prevention", test_case.preheat_backtrace },
{ "preheat_time", 30 },
{ "enable_pre_heating", test_case.pre_heating },
});
Print print;
Model model;
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ { { "extruder", 1 } }, { { "extruder", 2 } } };
Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config, &overrides);
GCodeProcessorResult result;
const std::string gcode = Test::gcode(print, &result);
REQUIRE((gcode.find("preheat T") != std::string::npos) == test_case.preheat_backtrace);
REQUIRE((gcode.find(GCodeProcessor::Machine_Start_GCode_End_Tag) != std::string::npos) == test_case.pre_heating);
REQUIRE(gcode.size() > GCodeProcessor::Output_Block_Size);
std::vector<size_t> newline_ends;
for (size_t i = gcode.find('\n'); i != std::string::npos; i = gcode.find('\n', i + 1))
newline_ends.push_back(i + 1);
REQUIRE(result.lines_ends.size() == newline_ends.size());
const auto difference = std::mismatch(result.lines_ends.begin(), result.lines_ends.end(), newline_ends.begin());
INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
CHECK(difference.first == result.lines_ends.end());
}
TEST_CASE("Reloaded moves name their lines in G-code a script rewrote in place", "[GCodeProcessor]")
{
Print print;
Model model;
Test::init_print({ Test::cube(20) }, print, model);
GCodeProcessorResult result;
const std::string gcode = Test::gcode(print, &result);
const auto exported_moves = result.moves;
// A script that prepends one comment and, writing in text mode on Windows, turns every LF into CRLF.
const std::string prepended = ";EDITED\r\n";
std::string edited = prepended;
for (const char c : gcode) {
if (c == '\n')
edited += '\r';
edited += c;
}
ScopedTemporaryFile temp(".gcode");
save_string_file(temp.path(), edited);
result.filename = temp.string();
print.reload_gcode_moves(&result);
std::vector<size_t> newline_ends;
for (size_t i = edited.find('\n'); i != std::string::npos; i = edited.find('\n', i + 1))
newline_ends.push_back(i + 1);
CHECK(result.lines_ends == newline_ends);
// Every move that came from a line now names the same line one further down.
REQUIRE(result.moves.size() == exported_moves.size());
const auto difference = std::mismatch(exported_moves.begin(), exported_moves.end(), result.moves.begin(),
[](const auto &exported, const auto &reloaded) {
return reloaded.gcode_id == (exported.gcode_id == 0 ? 0 : exported.gcode_id + 1);
});
INFO("first difference at move " << difference.first - exported_moves.begin());
CHECK(difference.first == exported_moves.end());
}
TEST_CASE("Rewritten G-code that cannot be re-read keeps the moves and hides the G-code window", "[GCodeProcessor]")
{
Print print;
Model model;
Test::init_print({ Test::cube(20) }, print, model);
GCodeProcessorResult result;
const std::string gcode = Test::gcode(print, &result);
const auto exported_moves = result.moves;
// A script that strips the trailing config block, which the G-code reader needs.
const size_t config_block = gcode.find("; CONFIG_BLOCK_START");
REQUIRE(config_block != std::string::npos);
ScopedTemporaryFile temp(".gcode");
save_string_file(temp.path(), gcode.substr(0, config_block));
result.filename = temp.string();
print.reload_gcode_moves(&result);
CHECK(result.lines_ends.empty());
REQUIRE(result.moves.size() == exported_moves.size());
CHECK(result.moves.back().gcode_id == exported_moves.back().gcode_id);
}
TEST_CASE("The plate's center of mass takes every extrusion of G-code without a print behind it", "[GCodeProcessor]")
{
GCodeProcessorResult result;
process_two_objects(result);
CHECK(result.object_masses.empty());
CHECK(result.body_masses.empty());
const GCodeProcessorResult::ObjectMass &plate = result.plate_mass;
REQUIRE(plate.printed_up_to_layer.size() == 2);
CHECK_THAT((center_of(plate.printed_up_to_layer.front()) -
weighted_center({ { 1., a_brim }, { 1., a_support }, { 1., a_wall_0 }, { 2., b_wall } })).norm(),
Catch::Matchers::WithinAbs(0., 1e-5));
CHECK_THAT((center_of(plate.printed_up_to_layer.back()) -
weighted_center({ { 1., a_brim }, { 1., a_support }, { 1., a_wall_0 }, { 2., b_wall }, { 1., a_wall_1 } })).norm(),
Catch::Matchers::WithinAbs(0., 1e-5));
// Each bead weighs its volume at the default density and spreads along its move, (a^2 + ab + b^2) / 3 for one from
// a to b: the brim from x 8 to 12 at y 8, the support at x 10 from y 20 to 30, A's walls from x 10 to 20 at y 10 and
// B's from x 50 to 60 at y 50 with twice the filament, all at z 0.1 but A's second wall at 0.3.
const GCodeProcessorResult::ObjectMass::Sum total = plate.total();
CHECK_THAT(total.mass / total.volume, Catch::Matchers::WithinRel(double(DEFAULT_FILAMENT_DENSITY), 1e-6));
const Vec3d second = total.second / total.mass;
CHECK_THAT(second.x(), Catch::Matchers::WithinRel((304. / 3. + 100. + 2. * 700. / 3. + 2. * 9100. / 3.) / 6., 1e-6));
CHECK_THAT(second.y(), Catch::Matchers::WithinRel((64. + 1900. / 3. + 2. * 100. + 2. * 2500.) / 6., 1e-6));
CHECK_THAT(second.z(), Catch::Matchers::WithinRel((5. * 0.01 + 0.09) / 6., 1e-5));
// The beads' center lines, brim and support included, from the first layer's bottom to the second's top.
CHECK_THAT((plate.box.min - Vec3d(8., 8., 0.)).norm(), Catch::Matchers::WithinAbs(0., 1e-5));
CHECK_THAT((plate.box.max - Vec3d(60., 50., 0.4)).norm(), Catch::Matchers::WithinAbs(0., 1e-5));
}
TEST_CASE("Each sliced cube's center of mass is its center, and the brim lowers the plate's printed one", "[GCodeProcessor]")
{
const bool copies = GENERATE(false, true);
INFO((copies ? "two copies of one cube" : "two cubes"));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "outer_only" }, { "brim_width", 5 }, { "combine_brims", 0 } });
std::vector<TriangleMesh> cubes{ Test::cube(20) };
if (!copies)
cubes.emplace_back(Test::cube(20));
Print print;
Model model;
Test::init_print(std::move(cubes), print, model, config, nullptr, true, copies ? 2 : 1);
GCodeProcessorResult result;
Test::gcode(print, &result);
CHECK(result.body_masses.empty());
REQUIRE(result.object_masses.size() == 2);
for (const ModelObject *object : model.objects)
for (size_t instance = 0; instance < object->instances.size(); ++instance) {
const Vec3d center = object->instance_bounding_box(instance).center();
const auto mass = std::min_element(result.object_masses.begin(), result.object_masses.end(), [&center](const auto &l, const auto &r) {
return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm();
});
// Off the center only by the infill's alignment and the top and bottom shells.
const Vec3d part = center_of(mass->total());
CHECK_THAT(part.x(), Catch::Matchers::WithinAbs(center.x(), 0.5));
CHECK_THAT(part.y(), Catch::Matchers::WithinAbs(center.y(), 0.5));
CHECK_THAT(part.z(), Catch::Matchers::WithinAbs(center.z(), 1.));
// The outer walls' center lines run half a line inside the cube's sides, of copies touching each other too.
const BoundingBoxf3 box = object->instance_bounding_box(instance);
for (int axis = 0; axis < 3; ++axis) {
CHECK_THAT(mass->box.min[axis], Catch::Matchers::WithinAbs(box.min[axis], 0.3));
CHECK_THAT(mass->box.max[axis], Catch::Matchers::WithinAbs(box.max[axis], 0.3));
}
}
GCodeProcessorResult::ObjectMass::Sum objects;
for (const GCodeProcessorResult::ObjectMass &object : result.object_masses)
objects.add(object.total());
const GCodeProcessorResult::ObjectMass::Sum plate = result.plate_mass.total();
CHECK(plate.mass > objects.mass);
CHECK(center_of(plate).z() < center_of(objects).z());
}
TEST_CASE("Each cube's raft is its support, centered below it", "[GCodeProcessor]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "raft_layers", 3 } });
Print print;
Model model;
Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.support_masses.size() == 2);
for (size_t i = 0; i < 2; ++i) {
const GCodeProcessorResult::ObjectMass::Sum support = result.support_masses[i].total();
const Vec3d object = center_of(result.object_masses[i].total());
REQUIRE(support.mass > 0.);
CHECK_THAT(center_of(support).x(), Catch::Matchers::WithinAbs(object.x(), 1.));
CHECK_THAT(center_of(support).y(), Catch::Matchers::WithinAbs(object.y(), 1.));
CHECK(center_of(support).z() < 1.);
}
}
TEST_CASE("A spiral vase cube counts all its extrusions, rising through each layer", "[GCodeProcessor]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "spiral_mode", 1 }, { "wall_loops", 1 },
{ "top_shell_layers", 0 }, { "sparse_infill_density", 0 } });
Print print;
Model model;
Test::init_print({ Test::cube(20) }, print, model, config);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.object_masses.size() == 1);
CHECK_THAT(result.object_masses.front().total().mass, Catch::Matchers::WithinRel(result.plate_mass.total().mass, 1e-6));
}
TEST_CASE("Each separate part of an assembly gets its center of mass, overlapping parts one", "[GCodeProcessor]")
{
const bool overlapping = GENERATE(false, true);
// Separated infills finds the bodies first, which the G-code export then takes.
const bool separated = GENERATE(false, true);
INFO((overlapping ? "overlapping parts" : "separate parts") << (separated ? ", separated infills" : ""));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "skirt_loops", 0 }, { "brim_type", "no_brim" }, { "separated_infills", separated ? 1 : 0 } });
TriangleMesh first = make_cube(20, 20, 20);
TriangleMesh second = make_cube(20, 20, 20);
first.translate(50, 50, 0);
second.translate(overlapping ? 60 : 90, 50, 0);
Print print;
Model model;
Test::init_print({ first }, print, model, config, nullptr, false);
model.objects.front()->add_volume(std::move(second), ModelVolumeType::MODEL_PART, false);
print.apply(model, config);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.object_masses.size() == 1);
CHECK(result.object_masses.front().assembly);
// One body is the object itself.
if (overlapping) {
CHECK(result.body_masses.empty());
return;
}
REQUIRE(result.body_masses.size() == 2);
CHECK(print.objects().front()->separated_body_bboxes().size() == (separated ? 2 : 0));
const ModelObject &object = *model.objects.front();
for (const ModelVolume *volume : object.volumes) {
const Vec3d center = volume->mesh().transformed_bounding_box(object.instances.front()->get_matrix() * volume->get_matrix()).center();
const auto body = std::min_element(result.body_masses.begin(), result.body_masses.end(), [&center](const auto &l, const auto &r) {
return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm();
});
const Vec3d part = center_of(body->total());
CHECK_THAT(part.x(), Catch::Matchers::WithinAbs(center.x(), 0.5));
CHECK_THAT(part.y(), Catch::Matchers::WithinAbs(center.y(), 0.5));
CHECK_THAT(part.z(), Catch::Matchers::WithinAbs(center.z(), 1.));
}
}
TEST_CASE("Each extrusion weighs its filament's density", "[GCodeProcessor]")
{
// Two like cubes, the second's filament three times as dense.
DynamicPrintConfig config = Test::multifilament_config(2, { { "filament_density", "1,3" }, { "skirt_loops", 0 }, { "brim_type", "no_brim" } });
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{ { { "extruder", 1 } }, { { "extruder", 2 } } };
Print print;
Model model;
Test::init_print({ Test::cube(20), Test::cube(20) }, print, model, config, &overrides);
GCodeProcessorResult result;
Test::gcode(print, &result);
REQUIRE(result.object_masses.size() == 2);
std::vector<const GCodeProcessorResult::ObjectMass *> masses;
for (const ModelObject *object : model.objects) {
const Vec3d center = object->instance_bounding_box(0).center();
masses.emplace_back(&*std::min_element(result.object_masses.begin(), result.object_masses.end(), [&center](const auto &l, const auto &r) {
return (center_of(l.total()) - center).squaredNorm() < (center_of(r.total()) - center).squaredNorm();
}));
}
CHECK_THAT(masses[1]->total().mass / masses[0]->total().mass, Catch::Matchers::WithinRel(3., 0.02));
// The plate's center lies three quarters of the way to the dense cube.
const Vec3d plate = center_of(result.plate_mass.total());
const Vec3d light = center_of(masses[0]->total());
const Vec3d dense = center_of(masses[1]->total());
CHECK_THAT((plate - light).dot(dense - light) / (dense - light).squaredNorm(), Catch::Matchers::WithinAbs(0.75, 0.01));
}