Compare commits

..
Author SHA1 Message Date
SoftFever 9f34f37c27 Merge branch 'main' into feature/slice-sweep-full-placeholder-coverage 2026-09-29 01:57:28 +08:00
SoftFever 185cfe4323 Slice-validate every custom G-code and filename_format in system profiles 2026-09-29 01:53:30 +08:00
SoftFever 78fb4f767a Fix the Wanhao France D12 template custom G-code failing to parse 2026-09-29 01:53:30 +08:00
SoftFever 05da6bbcc8 Remove unreachable ACE block from the Kobra X filament change G-code
The block ran only when flush_length_4 is -1392 and read ace_t_box_vector / ace_t_slot_vector, which only Anycubic's own slicer defines. It emitted comments only, so the printed G-code is unchanged.
2026-09-29 01:53:30 +08:00
26 changed files with 641 additions and 909 deletions
+5 -3
View File
@@ -183,9 +183,11 @@ jobs:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
artifact: ${{ github.sha }}-tests-macos-arm64
test-dir: build/arm64/tests
# Slice a two-colour cube through every shipped printer so all custom g-code
# (change_filament_gcode, machine start/end, etc.) is expanded - catches
# slicing regressions the static profile checks and unit tests can't see.
# Slice a two-colour cube through every shipped printer, and through every
# system process/filament whose templates no printer's own slice reaches, so
# every custom g-code and filename_format shipped is expanded (names in {if}
# branches not taken included) - catches slicing regressions the static
# profile checks and unit tests can't see.
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this
# covers src/engine PRs with the PR-built binary.
slice_check_linux:
+4 -2
View File
@@ -74,8 +74,10 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
# Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
# Slice a two-colour cube through every printer, and through every system process/filament whose
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
continue-on-error: true
-6
View File
@@ -40,12 +40,6 @@ The pattern is phased on fixed positions, so it lines up across layers and
across the regions of one layer. Rounding the corners with
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
Each region builds only the rows over its bounding box in that frame, and
outlines only the centerlines within `d1 / 2` of it, the ones whose outlines
reach it. Every row is monotone along its direction, so each outline is started
on the cap at the first end of its centerline, outside the region, and clipping
to the region cuts it only where it crosses the boundary.
## Cubic
Single-line Cubic draws the three families at the same spacing `h` and shifts
File diff suppressed because one or more lines are too long
@@ -112,7 +112,7 @@
"1"
],
"support_multi_bed_types": "1",
"template_custom_gcode": ";;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n{if curr_bed_type==\"Textured PEI Plate\"}\n SET_GCODE_OFFSET Z=-0.00\n{else}\nSET_GCODE_OFFSET Z=0.0\n{endif}\n{if curr_bed_type==\"Cool Plate\"}\n\n;available bed types are:\n;\"Cool Plate\"\n;\"Engineering Plate\"\n;\"High Temp Plate\"\n;\"Textured PEI Plate\"\n;;;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n",
"template_custom_gcode": ";;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n{if curr_bed_type==\"Textured PEI Plate\"}\n SET_GCODE_OFFSET Z=-0.00\n{else}\nSET_GCODE_OFFSET Z=0.0\n{endif}\n\n;available bed types are:\n;\"Cool Plate\"\n;\"Engineering Plate\"\n;\"High Temp Plate\"\n;\"Textured PEI Plate\"\n;;;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n",
"z_hop_types": "Normal Lift",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"default_print_profile": "0.16mm Optimal @Bambu Lab X1 Carbon 0.4 nozzle",
+5 -16
View File
@@ -7290,9 +7290,9 @@ int CLI::run(int argc, char **argv)
colors_out[color_idx] = ColorRGBA(float(rgb_color[0]) / 255.f, float(rgb_color[1]) / 255.f, float(rgb_color[2]) / 255.f, float(rgb_color[3]) / 255.f);
}
int glfw_major, glfw_minor, glfw_revision;
glfwGetVersion(&glfw_major, &glfw_minor, &glfw_revision);
BOOST_LOG_TRIVIAL(info) << boost::format("GLFW version %1%.%2%.%3%") % glfw_major % glfw_minor % glfw_revision;
int gl_major, gl_minor, gl_verbos;
glfwGetVersion(&gl_major, &gl_minor, &gl_verbos);
BOOST_LOG_TRIVIAL(info) << boost::format("opengl version %1%.%2%.%3%")%gl_major %gl_minor %gl_verbos;
bool thumbnail_opengl_ready = false;
glfwSetErrorCallback(glfw_callback);
@@ -7304,9 +7304,8 @@ int CLI::run(int argc, char **argv)
}
else {
BOOST_LOG_TRIVIAL(info) << "glfwInit Success."<< std::endl;
// Request OrcaSlicer's minimum OpenGL version, independently of the GLFW library version.
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, gl_major);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, gl_minor);
glfwWindowHint(GLFW_RED_BITS, 8);
glfwWindowHint(GLFW_GREEN_BITS, 8);
glfwWindowHint(GLFW_BLUE_BITS, 8);
@@ -7322,16 +7321,6 @@ int CLI::run(int argc, char **argv)
#endif
GLFWwindow* window = glfwCreateWindow(640, 480, "base_window", NULL, NULL);
#ifndef __WXMAC__
if (window == NULL) {
// Some drivers (e.g. older Mesa) only expose compatibility profile 3.0; take whatever they offer.
BOOST_LOG_TRIVIAL(warning) << "Failed to create OpenGL 3.3 compatibility context, retrying with driver default" << std::endl;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 1);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_ANY_PROFILE);
window = glfwCreateWindow(640, 480, "base_window", NULL, NULL);
}
#endif
if (window == NULL)
{
BOOST_LOG_TRIVIAL(error) << "Failed to create GLFW window; skipping thumbnail rendering for CLI export" << std::endl;
+273 -96
View File
@@ -20,6 +20,7 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/core.hpp>
@@ -32,6 +33,7 @@
#include <algorithm>
#include <fstream>
#include <iostream>
#include <set>
#include <string>
using namespace Slic3r;
@@ -160,37 +162,62 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
return rigid ? move : Vec2d::Zero();
}
// Slice one cube that switches from filament 1 to filament 2 partway up, so exactly one
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
// Slic3r::PlaceholderParserError from export.
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
// Slice cubes that switch from filament 1 to filament 2 partway up, so a filament change fires, then
// export. The change drives the printer's own change_filament_gcode: on a single-nozzle machine it rides
// the AMS prime tower (append_tcr), on a multi-nozzle machine it routes through the nozzle swap
// (set_extruder / append_tcr2) - the engine picks the path from the printer's topology, so one model
// covers both. The layer-by-layer slice also pauses at z 2 and runs the template custom g-code at z 7, so
// machine_pause_gcode and template_custom_gcode expand, and turns clumping detection on for a printer that
// ships wrapping_detection_gcode. The by-object slice prints two cubes in turn without a prime tower:
// printing_by_object_gcode fires before the second one, and the filament change goes through set_extruder.
// Either way the output file name is built after export, which expands filename_format with the final
// print statistics. An undefined placeholder in any shipped custom g-code throws
// Slic3r::PlaceholderParserError.
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
{
const Vec2d center = printable_area_center(cfg);
const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
const Vec2d center = printable_area_center(cfg);
std::vector<Vec2d> cube_mins;
if (by_object) {
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
// smooth timelapse. No skirt, so the two cubes' own skirts cannot overlap.
cfg.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(false));
cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
} else {
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
if (!cfg.opt_string("wrapping_detection_gcode").empty())
cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
cube_mins = {center - Vec2d(5., 5.) + place_wipe_tower(cfg, center)};
}
Model model;
Print print;
ModelObject *obj = model.add_object();
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
print.is_BBL_printer() = is_bbl;
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
for (const Vec2d &cube_min : cube_mins) {
TriangleMesh m = make_cube(10, 10, 10);
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
ModelObject *obj = model.add_object();
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
obj->add_volume(m);
obj->add_instance();
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
// trips Flow::with_spacing.
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
obj->ensure_on_bed();
print.auto_assign_extruders(obj);
}
// Custom g-codes per print_z apply to layer-by-layer printing only (ToolOrdering::assign_custom_gcodes).
if (!by_object)
model.plates_custom_gcodes[model.curr_plate_index].gcodes = {{2., CustomGCode::PausePrint, 1, "", ""},
{7., CustomGCode::Template, 1, "", ""}};
print.apply(model, cfg);
print.validate();
@@ -205,6 +232,7 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
in.close();
boost::system::error_code ec;
fs::remove(tmp, ec);
print.output_filename(); // names the export as the app does, expanding filename_format
return out;
}
@@ -264,18 +292,119 @@ void install_slice_context_log_sink()
logging::core::get()->add_sink(sink);
}
// Size the filament slots for the current selection and build the config every slice uses.
DynamicPrintConfig slice_config(PresetBundle &bundle)
{
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
if (pin_filament_map) {
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
for (size_t i = 0; i < fmap.size(); ++i)
fmap[i] = int(i % nozzles) + 1;
}
DynamicPrintConfig cfg = bundle.full_config();
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
// the variant mismatch this pinning avoids).
if (pin_filament_map)
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
if (self_index.size() != variants->size()) {
self_index.resize(variants->size());
for (size_t i = 0; i < self_index.size(); ++i)
self_index[i] = int(i) + 1;
}
}
return cfg;
}
// Slice the current selection and log any failure against `what` (the printer, and the extra preset if
// any). With an outdir, the g-code is also saved there as "<file_base>.gcode". Returns the g-code, or an
// empty string on failure.
std::string slice_selection(PresetBundle &bundle, const std::string &what, bool by_object, const std::string &outdir, const std::string &file_base)
{
try {
const std::string out = slice_two_color_cube_and_export(slice_config(bundle), bundle.is_bbl_vendor(), by_object);
if (!outdir.empty() && !out.empty())
save_string_file(fs::path(outdir) / (file_base + ".gcode"), out);
if (out.empty() || out.find("G1") == std::string::npos) {
BOOST_LOG_TRIVIAL(error) << what << " produced no g-code";
return {};
}
return out;
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << what << " failed to slice: " << ex.what();
return {};
}
}
// The templates a preset carries, as (key, text): every non-empty *_gcode text, and filename_format. Each
// value of a per-variant key counts, though a slice expands only the values of the variants its printer uses.
using Templates = std::set<std::pair<std::string, std::string>>;
Templates preset_templates(const DynamicPrintConfig &cfg)
{
Templates out;
for (const std::string &key : cfg.keys()) {
if (key != "filename_format" && !boost::algorithm::ends_with(key, "_gcode"))
continue;
const ConfigOption *opt = cfg.option(key);
if (opt->type() == coString) {
if (const std::string &text = static_cast<const ConfigOptionString *>(opt)->value; !text.empty())
out.emplace(key, text);
} else if (opt->type() == coStrings) {
for (const std::string &text : static_cast<const ConfigOptionStrings *>(opt)->values)
if (!text.empty())
out.emplace(key, text);
}
}
return out;
}
// Slice-and-export a two-colour cube through every shipped printer (optionally scoped to one vendor via
// -v). Unlike the static reference/placeholder checks, this expands every custom *_gcode - including
// change_filament_gcode at the one filament change - against the printer's fully-resolved config, so
// undefined-placeholder / invalid-flow bugs surface here. Reports every offending printer and returns 1
// if any failed, 0 otherwise. When outdir is non-empty, each printer's g-code is also written there as
// "<vendor>__<printer>.gcode" for manual inspection. The sweep is SEQUENTIAL by necessity:
// Print::process() keeps process-global state, so slicing printers concurrently in one process races
// even with per-slice Model+Print. Load in validation mode so the vendors are read straight from the -p
// profiles dir (no data_dir/system tree) and -v scoping is honoured for free.
// undefined-placeholder / invalid-flow bugs surface here. PlaceholderParser::check_inactive_branches is
// set for the sweep, so the names in {if} branches a slice does not take must resolve too.
//
// Each printer is sliced with its own default process and filament. Every other compatible system
// process and filament whose templates (filename_format, filament_start_gcode, ...) no slice has expanded
// yet is then sliced once on that printer - a filament that compatible_prints limits to another process
// with that process - so each template text shipped in any system preset is expanded once. That check runs on the first compatible printer the sweep reaches only, in that printer's hook
// contexts: a value-dependent error on another printer, or a hook call site there that sets fewer
// variables (a multi-nozzle tool change, say), is not covered. The first printer shipping each
// printing_by_object_gcode also slices two cubes by object, the only way that hook fires.
//
// Reports every failure and returns 1 if any slice failed, 0 otherwise. When outdir is non-empty, each
// g-code is also written there as "<vendor>__<printer>[__<preset>].gcode" for manual inspection. The
// sweep is SEQUENTIAL by necessity: Print::process() keeps process-global state, so slicing printers
// concurrently in one process races even with per-slice Model+Print. Load in validation mode so the
// vendors are read straight from the -p profiles dir (no data_dir/system tree) and -v scoping is honoured
// for free.
int slice_all_printers(const std::string &vendor, const std::string &outdir)
{
install_slice_context_log_sink();
PlaceholderParser::check_inactive_branches = true;
if (!outdir.empty()) {
boost::system::error_code ec;
@@ -323,7 +452,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
std::cout << "Slicing " << printers.size() << " printer preset(s)"
<< (vendor.empty() ? "" : " for vendor " + vendor) << "..." << std::endl;
int failures = 0;
Templates covered; // the process and filament templates some slice has expanded
auto cover = [&covered](const Preset &preset) {
const Templates templates = preset_templates(preset.config);
covered.insert(templates.begin(), templates.end());
};
auto is_covered = [&covered](const Preset &preset) {
const Templates templates = preset_templates(preset.config);
return std::includes(covered.begin(), covered.end(), templates.begin(), templates.end());
};
int failures = 0, extra_processes = 0, extra_filaments = 0, by_object_slices = 0;
for (const auto &[vendor_name, printer] : printers) {
g_slice_context = vendor_name + " / " + printer; // tag every engine log line from this slice
const bool selected = bundle.printers.select_preset_by_name(printer, /*force=*/true);
@@ -347,76 +485,115 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
continue;
}
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
// while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in
// type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL
// printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged).
const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1;
if (pin_filament_map) {
auto &fmap = bundle.project_config.option<ConfigOptionInts>("filament_map", true)->values;
for (size_t i = 0; i < fmap.size(); ++i)
fmap[i] = int(i % nozzles) + 1;
}
DynamicPrintConfig cfg = bundle.full_config();
cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable
// The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config
// stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce
// the variant mismatch this pinning avoids).
if (pin_filament_map)
cfg.set_key_value("filament_map_mode", new ConfigOptionEnum<FilamentMapMode>(fmmManual));
// full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic
// 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves
// filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments
// fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real
// 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The
// slice output is unaffected: the duplicated filament's per-variant values are identical to the first.
if (auto *variants = cfg.option<ConfigOptionStrings>("filament_extruder_variant")) {
auto &self_index = cfg.option<ConfigOptionInts>("filament_self_index", true)->values;
if (self_index.size() != variants->size()) {
self_index.resize(variants->size());
for (size_t i = 0; i < self_index.size(); ++i)
self_index[i] = int(i) + 1;
}
}
try {
const std::string out = slice_two_color_cube_and_export(cfg, bundle.is_bbl_vendor());
if (!outdir.empty() && !out.empty()) {
const fs::path f = fs::path(outdir) / (sanitize_filename(vendor_name) + "__" + sanitize_filename(printer) + ".gcode");
save_string_file(f, out);
}
if (out.empty() || out.find("G1") == std::string::npos) {
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" produced no g-code";
++failures;
} else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
// The filament change never rode the tower, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer
<< "\" sliced but the filament change never fired (no CP TOOLCHANGE START)";
++failures;
}
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" failed to slice: " << ex.what();
const std::string print_name = bundle.prints.get_selected_preset_name();
const std::string filament_name = bundle.filaments.get_selected_preset_name();
const std::string what = "Printer \"" + printer + "\"";
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
++failures;
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
// The filament change never rode the tower, so change_filament_gcode was not exercised.
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
++failures;
}
cover(bundle.prints.get_selected_preset());
cover(bundle.filaments.get_selected_preset());
const std::string &by_object_gcode = bundle.printers.get_selected_preset().config.opt_string("printing_by_object_gcode");
if (!by_object_gcode.empty() && covered.emplace("printing_by_object_gcode", by_object_gcode).second) {
++by_object_slices;
g_slice_context = vendor_name + " / " + printer + " / by object";
if (slice_selection(bundle, what + " printing by object", true, outdir, file_base + "__by_object").empty())
++failures;
}
// The first process of this printer that compatible_prints lets an otherwise incompatible filament
// use, judged as update_compatible() would with that process selected; empty if there is none.
const PresetWithVendorProfile printer_with_vendor = bundle.printers.get_edited_preset_with_vendor_profile();
auto limiting_process = [&bundle, &printer_with_vendor](const Preset &filament) -> std::string {
const auto *processes = filament.config.option<ConfigOptionStrings>("compatible_prints");
if (processes == nullptr || processes->values.empty())
return {};
const PresetWithVendorProfile filament_with_vendor = bundle.filaments.get_preset_with_vendor_profile(filament);
if (!is_compatible_with_printer(filament_with_vendor, printer_with_vendor))
return {};
for (const std::string &name : processes->values)
if (const Preset *process = bundle.prints.find_preset(name);
process != nullptr && process->is_visible && process->is_compatible &&
is_compatible_with_print(filament_with_vendor, bundle.prints.get_preset_with_vendor_profile(*process), printer_with_vendor))
return name;
return {};
};
// The compatibility flags are still this printer's, from update_compatible() above, which judged the
// filaments against the printer's own process. A hidden preset cannot be selected, so it is counted,
// not sliced.
std::vector<std::pair<const Preset *, std::string>> extras; // (extra preset, process to slice it with)
size_t hidden = 0;
for (const PresetCollection *presets : {&bundle.prints, &bundle.filaments})
for (const Preset &preset : presets->get_presets()) {
if (!preset.is_system || preset.is_default)
continue;
std::string process;
if (preset.is_compatible)
process = preset.type == Preset::TYPE_PRINT ? preset.name : print_name;
else if (preset.type == Preset::TYPE_FILAMENT)
process = limiting_process(preset);
if (process.empty() || is_covered(preset))
continue;
if (preset.is_visible)
extras.emplace_back(&preset, process);
else
++hidden;
}
if (hidden > 0)
BOOST_LOG_TRIVIAL(warning) << "Printer \"" << printer << "\": " << hidden
<< " compatible system preset(s) with unexpanded templates are hidden and were not sliced";
// Each extra slice swaps one preset into the printer's own selection. update_compatible() is not
// run again: it could swap filament slots. The filament change is not required to ride the tower
// here (a vase-mode process has none): the printer's own slice above already expanded its hooks.
for (const auto &[preset, process] : extras) {
if (is_covered(*preset))
continue; // an earlier extra slice expanded the same texts
const bool is_print = preset->type == Preset::TYPE_PRINT;
const std::string &filament = is_print ? filament_name : preset->name;
std::string extra = std::string(is_print ? "process" : "filament") + " \"" + preset->name + "\"";
if (!is_print && process != print_name)
extra += " and process \"" + process + "\"";
g_slice_context = vendor_name + " / " + printer + " / " + extra;
if (is_print)
++extra_processes;
else
++extra_filaments;
cover(*preset);
bundle.prints.select_preset_by_name(process, /*force=*/true);
bundle.filaments.select_preset_by_name(filament, /*force=*/true);
bundle.filament_presets.assign(1, bundle.filaments.get_selected_preset_name());
bundle.update_multi_material_filament_presets();
// select_preset_by_name() falls back to another preset, and still returns true, when it cannot select this one.
if (bundle.prints.get_selected_preset_name() != process || bundle.filaments.get_selected_preset_name() != filament) {
BOOST_LOG_TRIVIAL(error) << what << " could not select " << extra;
++failures;
continue;
}
if (slice_selection(bundle, what + " with " + extra, false, outdir, file_base + "__" + sanitize_filename(preset->name)).empty())
++failures;
}
// Leave the printer's own selection for the next printer to start from, as without the extra slices.
bundle.prints.select_preset_by_name(print_name, /*force=*/true);
bundle.filaments.select_preset_by_name(filament_name, /*force=*/true);
}
g_slice_context.clear();
const int slices = int(printers.size()) + extra_processes + extra_filaments + by_object_slices;
std::cout << "Sliced " << printers.size() << " printer preset(s), " << extra_processes << " more process preset(s), "
<< extra_filaments << " more filament preset(s) and " << by_object_slices << " printer(s) by object" << std::endl;
if (failures > 0) {
std::cout << failures << " of " << printers.size() << " printer preset(s) failed to slice" << std::endl;
std::cout << failures << " of " << slices << " slice(s) failed" << std::endl;
std::cout << "Validation failed" << std::endl;
return 1;
}
std::cout << "All " << printers.size() << " printer preset(s) sliced successfully" << std::endl;
std::cout << "All " << slices << " slice(s) succeeded" << std::endl;
std::cout << "Validation completed successfully" << std::endl;
return 0;
}
@@ -435,8 +612,8 @@ int main(int argc, char* argv[])
#endif
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer, and through every other system process and filament preset whose templates no printer's own slice expands, so every custom g-code and filename_format shipped is expanded, names in {if} branches not taken included (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each slice's g-code to this folder (as <vendor>__<printer>[__<preset>].gcode) for manual inspection. Optional.")
("check_filament_subtypes,f", po::bool_switch()->default_value(true), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
// clang-format on
+122 -94
View File
@@ -3094,6 +3094,7 @@ static std::vector<Vec2d> cubic_upper_level(double tau, double h, double period,
bool FillRectilinear::fill_surface_trapezoidal(
const Surface* surface,
FillParams params,
const std::initializer_list<SweepParams>& sweep_params,
Polylines& polylines_out,
int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic
{
@@ -3125,20 +3126,9 @@ bool FillRectilinear::fill_surface_trapezoidal(
expolygon.rotate(-base_angle, rotate_vector.second);
}
// Use extended object bounding box for consistent pattern across layers
BoundingBox bb = this->extended_object_bounding_box();
const size_t infill_layer_id = (surface->thickness_layers > 0) ? this->layer_id / surface->thickness_layers : this->layer_id;
// The triangular family turns by 120 degrees every layer, about the origin of the frame it is built in.
const size_t layer_mod = infill_layer_id % 3;
const double angle = layer_mod * 2.0 * M_PI / 3.0;
// Only build the rows over the surface, seen in the frame they are built in.
Polygon local = expolygon.contour;
if (Pattern_type != 0) {
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
if (layer_mod)
local.rotate(-angle);
}
BoundingBox cover = get_extents(local);
cover.offset(period);
switch (Pattern_type) {
case 0: // Grid / Trapezoidal
@@ -3158,31 +3148,21 @@ bool FillRectilinear::fill_surface_trapezoidal(
// Align bounding box to the grid, phased through the box center so separated infills align
// each part on itself (grid_center is the origin for a standalone object / feature off).
// Captured before the merge, which grows bb and would otherwise shift its center.
BoundingBox bb = this->extended_object_bounding_box();
const Point grid_center = bb.center();
bb.merge(align_to_grid(bb.min, Point(period, period), grid_center));
const coord_t xmin = bb.min.x();
const coord_t xmax = bb.max.x();
const coord_t ymin = bb.min.y();
auto transpose = [&grid_center](const Point &p) {
return Point(grid_center.x() + p.y() - grid_center.y(), grid_center.y() + p.x() - grid_center.x());
};
if (infill_layer_id % 2 == 1)
cover = BoundingBox(transpose(cover.min), transpose(cover.max));
const coord_t row_spacing = period / 2;
const coord_t first_x = xmin + (coord_t(std::floor(double(cover.min.x() - xmin) / period)) - 1) * period;
const coord_t last_x = xmin + (coord_t(std::ceil(double(cover.max.x() - xmin) / period)) + 1) * period;
const coord_t first_row = coord_t(std::floor(double(cover.min.y() - ymin) / row_spacing)) - 1;
const coord_t last_row = coord_t(std::ceil(double(cover.max.y() - ymin) / row_spacing)) + 1;
const coord_t ymax = bb.max.y();
// Create the two base row patterns once
Polyline base_row_normal;
base_row_normal.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid
base_row_normal.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid
Polyline base_row_flipped;
base_row_flipped.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid
base_row_flipped.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid
// Build rows on the same global period grid, limited to the surface cover.
for (coord_t x = first_x; x < last_x; x += period) {
// Build complete rows from xmin to xmax
for (coord_t x = xmin; x < xmax; x += period) {
// Normal row
base_row_normal.points.emplace_back(Point(x, d1 / 2)); // P0
base_row_normal.points.emplace_back(Point(x + d1 / 2, d1 / 2)); // P1
@@ -3198,14 +3178,13 @@ bool FillRectilinear::fill_surface_trapezoidal(
}
// Pre-allocate polylines
const size_t estimated_rows = size_t(last_row - first_row + 1);
const size_t estimated_rows = ((ymax - ymin) / (period / 2) + 1);
polylines.reserve(estimated_rows);
bool flip_vertical = (first_row % 2) != 0;
bool flip_vertical = false;
// Copy and translate only rows intersecting the surface cover.
for (coord_t row = first_row; row <= last_row; ++row) {
const coord_t y = ymin + row * row_spacing;
// Now just copy and translate vertically
for (coord_t y = ymin; y < ymax; y += period / 2) {
Polyline pl_row = flip_vertical ? base_row_flipped : base_row_normal;
// Translate all points vertically
@@ -3221,10 +3200,16 @@ bool FillRectilinear::fill_surface_trapezoidal(
// Orca: mirror across the diagonal through grid_center (not the origin), so the swapped
// layers stay aligned with the center-phased grid. For a standalone object / feature off,
// grid_center is the origin and this is a plain x/y swap.
if (infill_layer_id % 2 == 1)
for (Polyline& pl : polylines)
for (Point& p : pl.points)
p = transpose(p);
if (infill_layer_id % 2 == 1) {
for (Polyline& pl : polylines) {
for (Point& p : pl.points) {
const coord_t dx = p.x() - grid_center.x();
const coord_t dy = p.y() - grid_center.y();
p.x() = grid_center.x() + dy;
p.y() = grid_center.y() + dx;
}
}
}
break;
}
@@ -3245,24 +3230,40 @@ bool FillRectilinear::fill_surface_trapezoidal(
const coord_t d2_tri = coord_t(2.0 / std::sqrt(3.0) * d1);
const coord_t h = coord_t(0.5 * std::sqrt(3.0) * period); // height of triangle
// Keep the existing origin-anchored lattice, but generate only nearby tiles.
const coord_t x_min_aligned = (coord_t(std::floor(double(cover.min.x()) / period)) - 1) * period;
const coord_t x_max_aligned = (coord_t(std::ceil(double(cover.max.x()) / period)) + 1) * period;
const coord_t first_row = coord_t(std::floor(double(cover.min.y()) / h)) - 1;
const coord_t last_row = coord_t(std::ceil(double(cover.max.y()) / h)) + 1;
// Align bounding box to the grid
bb.merge(align_to_grid(bb.center(), Point(period,h)));
const size_t layer_mod = infill_layer_id % 3;
const double angle = layer_mod * 2.0 * M_PI / 3.0;
const Point rotation_center = bb.center();
const coord_t half_w = bb.size().x() / 2;
const coord_t half_h = bb.size().y() / 2;
// Compute how many full periods fit in each direction
const coord_t num_periods_x = coord_t(std::ceil(half_w / double(period)));
coord_t num_periods_y =coord_t(std::ceil(half_h / double(h)));
// Ensure an even number of rows so the pattern stays centered
if ((num_periods_y % 2) != 0)
++num_periods_y;
// Compute aligned limits (symmetric around the origin)
const coord_t x_min_aligned = -num_periods_x * period;
const coord_t x_max_aligned = num_periods_x * period;
const coord_t y_min_aligned = -num_periods_y * h;
const coord_t y_max_aligned = num_periods_y * h;
// Pre-allocate estimated number of polylines
const size_t estimated_rows = size_t(last_row - first_row + 1);
const size_t estimated_polylines = estimated_rows * 2; // base line + trapezoid line per row
const size_t estimated_rows = (y_max_aligned - y_min_aligned) / h + 2;
const size_t estimated_polylines = (estimated_rows + 1) * 2; // base line + trapezoid line per row
polylines.reserve(estimated_polylines);
// Create the two base row templates once
Polyline base_line_template;
base_line_template.points.reserve(2); // 2 points for base line
Polyline trapezoid_row_normal;
trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid
trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid
Polyline trapezoid_row_shifted;
trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid
trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid
// Build base line template (from x_min_aligned to x_max_aligned)
base_line_template.points.emplace_back(Point(x_min_aligned, 0));
base_line_template.points.emplace_back(Point(x_max_aligned, 0));
@@ -3282,10 +3283,10 @@ bool FillRectilinear::fill_surface_trapezoidal(
for (auto& p : trapezoid_row_shifted.points)
p.y() = h - p.y();
bool shift_row = false;
// Generate pattern by copying and translating templates vertically
bool shift_row = (first_row % 2) != 0;
for (coord_t row = first_row; row <= last_row; ++row) {
const coord_t y = row * h;
for (coord_t y = y_min_aligned; y < y_max_aligned; y += h) {
// Base line - copy and translate
Polyline base_line = base_line_template;
for (Point& p : base_line.points) {
@@ -3305,6 +3306,12 @@ bool FillRectilinear::fill_surface_trapezoidal(
shift_row = !shift_row;
}
// Rotate around origin (0,0)
if (layer_mod)
for (auto& pl : polylines)
pl.rotate(angle, Point(0,0));
break;
}
@@ -3318,25 +3325,36 @@ bool FillRectilinear::fill_surface_trapezoidal(
const coord_t d1_half_base = d1_half / std::sqrt(3.0);
const coord_t half_period = period / 2;
const coord_t quarter_period = period / 4;
const coord_t row_y_offset = tri_height - (2 * tri_height) / 3;
// Keep the lattice anchored at the origin while generating only tiles around this surface.
const int64_t first_tile = int64_t(std::floor(double(cover.min.x()) / period)) - 1;
const int64_t last_tile = int64_t(std::ceil(double(cover.max.x()) / period)) + 1;
const int64_t first_row = int64_t(std::floor(double(cover.min.y() - row_y_offset) / hex_height)) - 1;
const int64_t last_row = int64_t(std::ceil(double(cover.max.y() - row_y_offset) / hex_height)) + 1;
bb.merge(align_to_grid(bb.center(), Point(period, tri_height)));
const size_t layer_mod = infill_layer_id % 3;
const double angle = layer_mod * 2.0 * M_PI / 3.0;
const size_t estimated_rows = size_t(last_row - first_row + 1);
const size_t estimated_polylines = estimated_rows * 2;
const coord_t half_w = bb.size().x() / 2;
const coord_t half_h = bb.size().y() / 2;
const coord_t num_periods_x = coord_t(std::ceil(half_w / double(period)));
coord_t num_periods_y = coord_t(std::ceil(half_h / double(hex_height)));
if ((num_periods_y % 2) != 0)
++num_periods_y;
const coord_t x_alignment_shift = half_period;
const coord_t y_alignment_shift = (2 * tri_height) / 3;
const coord_t x_min_aligned = -num_periods_x * period - x_alignment_shift;
const coord_t x_max_aligned = num_periods_x * period - x_alignment_shift;
const coord_t y_min_aligned = -num_periods_y * hex_height - y_alignment_shift;
const coord_t y_max_aligned = num_periods_y * hex_height - y_alignment_shift;
const size_t estimated_rows = (y_max_aligned - y_min_aligned) / hex_height + 2;
const size_t estimated_polylines = (estimated_rows + 1) * 2;
polylines.reserve(estimated_polylines);
Polyline star_row_normal;
star_row_normal.points.reserve(size_t(last_tile - first_tile) * 7);
star_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7);
Polyline star_row_mirrored;
star_row_mirrored.points.reserve(size_t(last_tile - first_tile) * 7);
star_row_mirrored.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7);
for (int64_t tile = first_tile; tile < last_tile; ++tile) {
const coord_t x = coord_t(tile * period) - half_period;
for (coord_t x = x_min_aligned; x < x_max_aligned; x += period) {
star_row_normal.points.emplace_back(Point(x, hex_height)); // P0
star_row_normal.points.emplace_back(Point(x + quarter_period - d1, hex_height)); // P1
star_row_normal.points.emplace_back(Point(x + quarter_period + d1_half, hex_height - chamfer_height)); // P2
@@ -3350,7 +3368,9 @@ bool FillRectilinear::fill_surface_trapezoidal(
for (auto& p : star_row_mirrored.points)
p.y() = hex_height - p.y();
size_t pair_idx = 0;
const coord_t global_x_shift = half_period;
const coord_t global_y_shift = tri_height;
auto append_row_with_shift = [&polylines](const Polyline& row_template, coord_t x_shift, coord_t y_shift) {
Polyline row = row_template;
for (Point& p : row.points) {
@@ -3361,12 +3381,16 @@ bool FillRectilinear::fill_surface_trapezoidal(
polylines.emplace_back(std::move(row));
};
for (int64_t row = first_row; row <= last_row; ++row) {
const coord_t y = coord_t(row * hex_height) + row_y_offset;
const coord_t x_shift = (row % 2 == 0) ? 0 : half_period;
append_row_with_shift(star_row_normal, x_shift + global_x_shift, y);
append_row_with_shift(star_row_mirrored, x_shift + global_x_shift, y);
for (coord_t y = y_min_aligned; y < y_max_aligned; y += hex_height, ++pair_idx) {
const coord_t x_shift = (pair_idx % 2 == 0) ? 0 : half_period;
append_row_with_shift(star_row_normal, x_shift + global_x_shift, y + global_y_shift);
append_row_with_shift(star_row_mirrored, x_shift + global_x_shift, y + global_y_shift);
}
if (layer_mod)
for (auto& pl : polylines)
pl.rotate(angle, Point(0, 0));
break;
}
@@ -3383,9 +3407,19 @@ bool FillRectilinear::fill_surface_trapezoidal(
for (Vec2d &p : levels.front())
p.y() = h - p.y();
const size_t layer_mod = infill_layer_id % 3;
const double angle = layer_mod * 2.0 * M_PI / 3.0;
// Only cover the surface, seen in the frame the pattern is built in.
ExPolygon local = expolygon;
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
if (layer_mod)
local.rotate(-angle);
BoundingBox cover = get_extents(local);
cover.offset(period);
const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1;
const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1;
polylines.reserve(size_t(n_max - n_min + 1) * levels.size());
for (int64_t n = n_min; n <= n_max; ++n) {
const double x_off = (n & 1) ? 0.5 * period : 0.;
const double base = y0 + double(n) * h - tau;
@@ -3401,6 +3435,11 @@ bool FillRectilinear::fill_surface_trapezoidal(
polylines.emplace_back(std::move(row));
}
}
if (layer_mod)
for (Polyline &pl : polylines)
pl.rotate(angle, Point(0, 0));
break;
}
@@ -3409,38 +3448,21 @@ bool FillRectilinear::fill_surface_trapezoidal(
break;
}
// Orca: cases 1 to 3 anchor the pattern at the origin, so on their own they phase to the global
// origin and every part shares one grid. Shift the pattern onto the box center
// this->bounding_box carries, so separated infills align each part on itself. The center is the
// origin for a standalone object (or when the feature is off), making the shift a no-op there.
// Orca: cases 1 & 2 build the pattern symmetrically around the origin, so on their own they
// phase to the global origin and every part shares one grid. Shift the pattern onto the box
// center this->bounding_box carries, so separated infills align each part on itself. The center
// is the origin for a standalone object (or when the feature is off), making this a no-op there.
if (Pattern_type != 0)
for (Polyline &pl : polylines) {
if (layer_mod)
pl.rotate(angle, Point(0, 0));
for (Polyline &pl : polylines)
pl.translate(rotate_vector.second);
}
// Orca: round the corners of the trapezoids. The straight base lines of the triangular family
// have no corner to round.
smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution));
// Only the centerlines within d1 / 2 of the surface have outlines reaching it.
polylines = intersection_pl(std::move(polylines), offset(expolygon, float(d1 / 2)));
// Apply multiline fill
multiline_fill(polylines, params, spacing);
// Start each outline on the cap at the first end of its path, outside the surface, so clipping splits it only there.
const Vec2d row_dir = Pattern_type != 0 ? Vec2d(std::cos(angle), std::sin(angle)) : infill_layer_id % 2 ? Vec2d::UnitY() : Vec2d::UnitX();
for (Polyline &pl : polylines)
if (pl.size() > 3 && pl.first_point() == pl.last_point()) {
pl.points.pop_back();
std::rotate(pl.points.begin(), std::min_element(pl.points.begin(), pl.points.end(), [&row_dir](const Point &a, const Point &b) {
return row_dir.dot(a.cast<double>()) < row_dir.dot(b.cast<double>());
}), pl.points.end());
pl.points.emplace_back(pl.points.front());
}
// Contract surface polygon by half line width to avoid excesive overlap with perimeter
ExPolygons contracted = offset_ex(expolygon, -float(scale_(0.5 * this->spacing)));
@@ -3523,7 +3545,9 @@ Polylines FillGrid::fill_surface(const Surface *surface, const FillParams &param
if (params.multiline > 1) {
// Experimental trapezoidal grid
if (!this->fill_surface_trapezoidal(
surface, params, polylines_out, 0))
surface, params,
{ { 0.f, 0.f }, { float(M_PI / 2.), 0.f } },
polylines_out,0))
BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed.";
} else {
@@ -3563,7 +3587,9 @@ Polylines FillTriangles::fill_surface(const Surface *surface, const FillParams &
if (params.multiline > 1) {
// Experimental trapezoidal grid
if (!this->fill_surface_trapezoidal(
surface, params, polylines_out, 1))
surface, params,
{ { 0.f, 0.f }, { float(M_PI / 2.), 0.f } },
polylines_out,1))
BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed.";
} else {
@@ -3582,7 +3608,9 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams &para
Polylines polylines_out;
if (params.multiline > 1) {
if (!this->fill_surface_trapezoidal(
surface, params, polylines_out, 2))
surface, params,
{{0.f, 0.f}, {float(M_PI / 3.), 0.f}, {float(2. * M_PI / 3.), float((3. / 2.) * this->spacing * params.multiline / params.density)}},
polylines_out, 2))
BOOST_LOG_TRIVIAL(error) << "FillStars::fill_surface_trapezoidal() failed.";
} else {
if (! this->fill_surface_by_multilines(
@@ -3598,7 +3626,7 @@ Polylines FillCubic::fill_surface(const Surface *surface, const FillParams &para
{
Polylines polylines_out;
if (params.multiline > 1) {
if (!this->fill_surface_trapezoidal(surface, params, polylines_out, 3))
if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3))
BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed.";
return polylines_out;
}
+1 -1
View File
@@ -29,7 +29,7 @@ protected:
float pattern_shift;
};
bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out);
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type);
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out,int Pattern_type);
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox extended_object_bounding_box() const;
+6 -16
View File
@@ -311,17 +311,14 @@ bool PrusaFileParser::check_3mf_from_prusa(const std::string filename)
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&archive, model_file_index, &stat)) goto EXIT;
// expat sizes its buffer with an int, so a larger entry cannot be parsed in one piece.
if (stat.m_uncomp_size > static_cast<mz_uint64>(std::numeric_limits<int>::max())) goto EXIT;
const int xml_size = static_cast<int>(stat.m_uncomp_size);
void *parser_buffer = XML_GetBuffer(m_parser, xml_size);
void *parser_buffer = XML_GetBuffer(m_parser, (int) stat.m_uncomp_size);
if (parser_buffer == nullptr) goto EXIT;
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t) stat.m_uncomp_size, 0);
if (res == 0) goto EXIT;
XML_ParseBuffer(m_parser, xml_size, 1);
XML_ParseBuffer(m_parser, (int) stat.m_uncomp_size, 1);
}
}
@@ -1360,26 +1357,19 @@ ModelVolumeType type_from_string(const std::string &s)
XML_SetUserData(m_xml_parser, (void*)this);
XML_SetElementHandler(m_xml_parser, _3MF_Importer::_handle_start_config_xml_element, _3MF_Importer::_handle_end_config_xml_element);
// expat sizes its buffer with an int, so a larger entry cannot be parsed in one piece.
if (stat.m_uncomp_size > static_cast<mz_uint64>(std::numeric_limits<int>::max())) {
add_error("Found invalid size");
return false;
}
const int xml_size = static_cast<int>(stat.m_uncomp_size);
void* parser_buffer = XML_GetBuffer(m_xml_parser, xml_size);
void* parser_buffer = XML_GetBuffer(m_xml_parser, (int)stat.m_uncomp_size);
if (parser_buffer == nullptr) {
add_error("Unable to create buffer");
return false;
}
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t)stat.m_uncomp_size, 0);
if (res == 0) {
add_error("Error while reading config data to buffer");
return false;
}
if (!XML_ParseBuffer(m_xml_parser, xml_size, 1)) {
if (!XML_ParseBuffer(m_xml_parser, (int)stat.m_uncomp_size, 1)) {
char error_buf[1024];
::sprintf(error_buf, "Error (%s) while parsing xml file at line %d", XML_ErrorString(XML_GetErrorCode(m_xml_parser)), (int)XML_GetCurrentLineNumber(m_xml_parser));
add_error(error_buf);
+5 -12
View File
@@ -1633,7 +1633,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
while (it != m_plater_data.end())
{
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
if (it->first > m_plater_data.size())
{
add_error("invalid plate index");
return false;
@@ -2316,7 +2316,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
while (it != m_plater_data.end())
{
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
if (it->first > m_plater_data.size())
{
add_error("invalid plate index");
return false;
@@ -2512,26 +2512,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
XML_SetEntityDeclHandler(m_xml_parser, nullptr);
XML_SetExternalEntityRefHandler(m_xml_parser, nullptr);
// expat sizes its buffer with an int, so a larger entry cannot be parsed in one piece.
if (stat.m_uncomp_size > static_cast<mz_uint64>(std::numeric_limits<int>::max())) {
add_error("Found invalid size");
return false;
}
const int xml_size = static_cast<int>(stat.m_uncomp_size);
void* parser_buffer = XML_GetBuffer(m_xml_parser, xml_size);
void* parser_buffer = XML_GetBuffer(m_xml_parser, (int)stat.m_uncomp_size);
if (parser_buffer == nullptr) {
add_error("Unable to create buffer");
return false;
}
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t)stat.m_uncomp_size, 0);
if (res == 0) {
add_error("Error while reading config data to buffer");
return false;
}
if (!XML_ParseBuffer(m_xml_parser, xml_size, 1)) {
if (!XML_ParseBuffer(m_xml_parser, (int)stat.m_uncomp_size, 1)) {
char error_buf[1024];
::snprintf(error_buf, 1024, "Error (%s) while parsing xml file at line %d", XML_ErrorString(XML_GetErrorCode(m_xml_parser)), (int)XML_GetCurrentLineNumber(m_xml_parser));
add_error(error_buf);
+1 -5
View File
@@ -3750,11 +3750,7 @@ void FacetsAnnotation::set_triangle_from_string(int triangle_id, const std::stri
m_data.bitstream.insert(m_data.bitstream.end(), bool(dec & (1 << i)));
}
if (!m_data.update_used_states(bitstream_start_idx)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": dropping malformed paint data of triangle " << triangle_id;
m_data.bitstream.resize(bitstream_start_idx);
m_data.triangles_to_split.pop_back();
}
m_data.update_used_states(bitstream_start_idx);
}
bool FacetsAnnotation::equals(const FacetsAnnotation &other) const
+34 -11
View File
@@ -851,6 +851,8 @@ namespace client
// If true, the macro processor will evaluate just a boolean condition using the full expressive power of the macro processor.
bool just_boolean_expression = false;
std::string error_message;
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
mutable std::set<std::string> inactive_local_variables;
// Table to translate symbol tag to a human readable error message.
static std::map<std::string, std::string> tag_to_error_message;
@@ -892,6 +894,8 @@ namespace client
}
// Inside a block, which is conditionally suppressed?
bool skipping() const { return m_depth_suppressed > 0; }
// Are variable names resolved inside the suppressed blocks too?
bool check_inactive_names() const { return PlaceholderParser::check_inactive_branches && ! just_boolean_expression; }
const ConfigOption* optptr(const t_config_option_key &opt_key) const override
{
@@ -927,7 +931,7 @@ namespace client
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
{
if (ctx->skipping())
if (ctx->skipping() && ! ctx->check_inactive_names())
return;
std::string opt_key_str(opt_key.begin(), opt_key.end());
@@ -949,7 +953,9 @@ namespace client
}
}
if (opt == nullptr)
ctx->throw_exception("Variable does not exist", opt_key);
ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key);
if (ctx->skipping())
return;
if (opt->is_scalar()) {
if (opt->is_nil())
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
@@ -972,9 +978,10 @@ namespace client
IteratorRange &opt_vector_index,
std::string &output)
{
if (ctx->skipping())
if (ctx->skipping() && ! ctx->check_inactive_names())
return;
const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist";
std::string opt_key_str(opt_key.begin(), opt_key.end());
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
if (opt == nullptr) {
@@ -984,18 +991,20 @@ namespace client
opt = ctx->resolve_symbol(opt_key_str);
}
if (opt == nullptr)
ctx->throw_exception("Variable does not exist", opt_key);
ctx->throw_exception(not_found, opt_key);
}
if (! opt->is_vector())
ctx->throw_exception("Trying to index a scalar variable", opt_key);
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
if (opt_index == nullptr)
ctx->throw_exception(not_found, opt_key);
if (opt_index->type() != coInt)
ctx->throw_exception("Indexing variable has to be integer", opt_key);
if (ctx->skipping())
return;
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt_key);
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
if (opt_index == nullptr)
ctx->throw_exception("Variable does not exist", opt_key);
if (opt_index->type() != coInt)
ctx->throw_exception("Indexing variable has to be integer", opt_key);
int idx = opt_index->getInt();
if (idx < 0)
ctx->throw_exception("Negative vector index", opt_key);
@@ -1021,6 +1030,13 @@ namespace client
output.writable = true;
}
output.opt = opt;
} else if (ctx->check_inactive_names()) {
// Only check the name. Back tracking may resolve the same identifier twice, so there are no side effects.
const std::string key{ opt_key.begin(), opt_key.end() };
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
ctx->inactive_local_variables.count(key) == 0 &&
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key);
}
output.it_range = opt_key;
}
@@ -1426,6 +1442,13 @@ namespace client
out.opt = ctx->config_local.optptr(key);
}
out.name = std::move(key);
} else if (ctx->check_inactive_names()) {
// Declared in a branch that is not taken: the name still counts as defined for the names that follow.
std::string key(it_range.begin(), it_range.end());
if (global_variable && ctx->context_data != nullptr)
ctx->context_data->inactive_global_variables.insert(std::move(key));
else
ctx->inactive_local_variables.insert(std::move(key));
}
out.it_range = it_range;
}
@@ -1660,8 +1683,8 @@ namespace client
const OptWithPos &rhs)
{
if (ctx->skipping())
// Skipping, continue parsing.
return true;
// Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference.
return false;
if (lhs.opt) {
assert(lhs.opt->is_vector());
+8
View File
@@ -4,6 +4,7 @@
#include "libslic3r.h"
#include <map>
#include <random>
#include <set>
#include <string>
#include <string_view>
#include <vector>
@@ -24,8 +25,15 @@ public:
// If defined, then this dictionary is used by the scripts to define user variables and persist them
// between PlaceholderParser evaluations.
std::unique_ptr<DynamicConfig> global_config;
// Global variables declared in {if} branches that were not taken, see check_inactive_branches.
std::set<std::string> inactive_global_variables;
};
// Orca: when set, variable names inside {if} branches that are not taken must resolve too, so a single
// expansion checks every branch of a template. Only the profile validator's slice sweep sets it.
// It does not apply to evaluate_boolean_expression(), where an error reads as "compatible".
static inline bool check_inactive_branches = false;
PlaceholderParser(const DynamicConfig *external_config = nullptr);
void clear_config() { m_config.clear(); }
+43 -68
View File
@@ -1778,13 +1778,6 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
return out.data;
}
// A split code keeps the split side (one split) or the kept side (two splits) in its upper two
// bits, where 3 is not a side. The value is ignored for a three-side split.
static bool split_code_valid(int code)
{
return (code & 0b11) == 3 || (code >> 2) != 3;
}
void TriangleSelector::deserialize(const TriangleSplittingData &data,
bool needs_reset,
EnforcerBlockerType max_ebt,
@@ -1819,12 +1812,11 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
for (auto [triangle_id, ibit] : data.triangles_to_split) {
assert(triangle_id < int(m_triangles.size()));
// Set when the bitstream runs out or holds an impossible split before this triangle's tree is complete.
bool corrupt = false;
auto next_nibble = [&data, &ibit = ibit, &corrupt]() {
assert(ibit < int(data.bitstream.size()));
auto next_nibble = [&data, &ibit = ibit]() {
int n = 0;
if (! data.read_nibble(ibit, n))
corrupt = true;
for (int i = 0; i < 4; ++ i)
n |= data.bitstream[ibit ++] << i;
return n;
};
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
@@ -1843,10 +1835,6 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
bool is_split = num_of_children != 0;
// Only valid if not is_split.
auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2));
if (is_split && ! split_code_valid(code))
corrupt = true;
if (corrupt)
break;
// BBS
if (state == to_delete_filament)
@@ -1861,7 +1849,7 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
}
// Only valid if is_split.
int special_side = num_of_split_sides == 3 ? 0 : code >> 2;
int special_side = code >> 2;
// Take care of the first iteration separately, so handling of the others is simpler.
if (parents.empty()) {
@@ -1916,55 +1904,47 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
if (parents.empty())
break;
}
if (corrupt) {
// Every split above allocated all of its children, so the partial tree unwinds cleanly.
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": malformed paint data, dropping paint of triangle " << triangle_id;
undivide_triangle(triangle_id);
m_triangles[triangle_id].set_state(EnforcerBlockerType::NONE);
}
}
}
bool TriangleSelector::TriangleSplittingData::update_used_states(const size_t bitstream_start_idx) {
int ibit = static_cast<int>(bitstream_start_idx);
uint64_t states = 0;
do {
// Walk one triangle's tree depth-first, counting the nodes still to be read; a split node adds its children.
for (int pending_nodes = 1; pending_nodes > 0; --pending_nodes) {
int code;
if (!this->read_nibble(ibit, code))
return false;
void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bitstream_start_idx) {
assert(bitstream_start_idx < this->bitstream.size());
assert(!this->bitstream.empty() && this->bitstream.size() != bitstream_start_idx);
assert((this->bitstream.size() - bitstream_start_idx) % 4 == 0);
if (const int num_of_split_sides = code & 0b11; num_of_split_sides != 0) {
if (!split_code_valid(code))
return false;
pending_nodes += num_of_split_sides + 1;
continue;
}
if (this->bitstream.empty() || this->bitstream.size() == bitstream_start_idx)
return;
int facet_state = code >> 2;
if (facet_state == 0b11) {
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
int nibble;
if (!this->read_nibble(ibit, nibble))
return false;
facet_state = nibble + 3;
if (nibble == 0b1111) {
if (!this->read_nibble(ibit, nibble))
return false;
facet_state = nibble + 18;
}
}
states |= uint64_t(1) << facet_state;
size_t nibble_idx = bitstream_start_idx;
auto read_next_nibble = [&data_bitstream = std::as_const(this->bitstream), &nibble_idx]() -> uint8_t {
assert(nibble_idx + 3 < data_bitstream.size());
uint8_t code = 0;
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx)
code |= data_bitstream[nibble_idx++] << bit_idx;
return code;
};
while (nibble_idx < this->bitstream.size()) {
const uint8_t code = read_next_nibble();
if (const bool is_split = (code & 0b11) != 0; is_split)
continue;
uint8_t facet_state;
if ((code & 0b1100) == 0b1100) {
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
const uint8_t nibble = read_next_nibble();
facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3);
} else {
facet_state = code >> 2;
}
} while (static_cast<size_t>(ibit) < this->bitstream.size());
assert(facet_state < this->used_states.size());
if (facet_state >= this->used_states.size())
continue;
// The leaf encoding tops out at state 33, so every state fits the 64-bit mask.
for (size_t state_idx = 0; state_idx < std::min<size_t>(this->used_states.size(), 64); ++state_idx)
if (states & (uint64_t(1) << state_idx))
this->used_states[state_idx] = true;
return true;
this->used_states[facet_state] = true;
}
}
// Lightweight variant of deserialization, which only tests whether a face of test_state exists.
@@ -1976,12 +1956,11 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
for (const TriangleBitStreamMapping &triangle_id_and_ibit : data.triangles_to_split) {
int ibit = triangle_id_and_ibit.bitstream_start_idx;
// Stop reading a triangle whose stream is truncated.
bool truncated = false;
auto next_nibble = [&data, &ibit = ibit, &truncated]() {
assert(ibit < int(data.bitstream.size()));
auto next_nibble = [&data, &ibit = ibit]() {
int n = 0;
if (! data.read_nibble(ibit, n))
truncated = true;
for (int i = 0; i < 4; ++ i)
n |= data.bitstream[ibit ++] << i;
return n;
};
// < 0 -> negative of a number of children
@@ -1999,8 +1978,6 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
};
int state = num_children_or_state();
if (truncated)
continue;
if (state < 0) {
// Root is split.
parents_children.clear();
@@ -2008,8 +1985,6 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
do {
if (-- parents_children.back() >= 0) {
int state = num_children_or_state();
if (truncated)
break;
if (state < 0)
// Child is split.
parents_children.emplace_back(- state);
+2 -14
View File
@@ -297,20 +297,8 @@ public:
std::fill(used_states.begin(), used_states.end(), false);
}
// Update used states from the triangle trees stored between bitstream_start_idx and the end of the bitstream.
// Returns false and leaves used states untouched if a tree is truncated or malformed.
bool update_used_states(size_t bitstream_start_idx);
// Read the 4-bit code at bit index ibit (LSB first) and advance ibit past it.
// Returns false without advancing when fewer than 4 bits remain.
bool read_nibble(int &ibit, int &nibble) const {
if (ibit < 0 || static_cast<size_t>(ibit) + 4 > bitstream.size())
return false;
nibble = 0;
for (int i = 0; i < 4; ++i)
nibble |= static_cast<int>(bitstream[ibit++]) << i;
return true;
}
// Update used states based on the bitstream. It just iterated over the bitstream from the bitstream_start_idx till the end.
void update_used_states(size_t bitstream_start_idx);
private:
friend class cereal::access;
-15
View File
@@ -285,21 +285,6 @@ inline std::string sanitize_filename(const std::string &filename){
const std::regex special_chars("[/\\\\:*?\"<>|]");
return std::regex_replace(filename, special_chars, "_");
}
// Reduce an untrusted, possibly path-qualified name to a single sanitized file name.
// Returns an empty string when nothing usable remains.
inline std::string sanitize_file_basename(const std::string &name){
const size_t sep = name.find_last_of("/\\");
const std::string base = sanitize_filename(sep == std::string::npos ? name : name.substr(sep + 1));
// Names made only of dots and spaces refer to the folder or its parent, or are stripped to nothing on Windows.
return base.find_first_not_of(". ") == std::string::npos ? std::string() : base;
}
// Marker file a download of this process writes to before it is renamed to filename.
boost::filesystem::path download_marker_path(const boost::filesystem::path &dest_folder, const std::string &filename);
// Finds a sanitized variant of filename, "name(N).ext" if needed, that neither an entry of dest_folder
// nor the download marker of another download uses. The marker at ignored_marker does not count.
// Returns true and the name in result, or false and the last name tried.
bool find_unused_filename(const boost::filesystem::path &dest_folder, const std::string &filename,
const boost::filesystem::path &ignored_marker, std::string &result);
// File path / name / extension splitting utilities, working with UTF-8,
// to be published to Perl.
namespace PerlUtils {
-25
View File
@@ -1321,31 +1321,6 @@ unsigned get_current_pid()
#endif
}
boost::filesystem::path download_marker_path(const boost::filesystem::path &dest_folder, const std::string &filename)
{
return dest_folder / (filename + "." + std::to_string(get_current_pid()) + ".download");
}
bool find_unused_filename(const boost::filesystem::path &dest_folder, const std::string &filename,
const boost::filesystem::path &ignored_marker, std::string &result)
{
// Probe the name that will be written, so a name the sanitizing maps onto an existing file is versioned too.
const std::string sanitized = sanitize_filename(filename);
const std::string extension = boost::filesystem::path(sanitized).extension().string();
const std::string stem = sanitized.substr(0, sanitized.size() - extension.size());
auto is_used = [&](const std::string &name) {
const boost::filesystem::path marker = download_marker_path(dest_folder, name);
return boost::filesystem::exists(dest_folder / name) || (marker != ignored_marker && boost::filesystem::exists(marker));
};
result = sanitized;
for (size_t version = 1; is_used(result); ++version) {
if (version > 999)
return false;
result = stem + "(" + std::to_string(version) + ")" + extension;
}
return true;
}
std::string per_user_temp_id()
{
#ifdef WIN32
+32 -51
View File
@@ -71,6 +71,17 @@ bool FileGet::is_subdomain(const std::string& url, const std::string& domain)
return false;
}
namespace {
unsigned get_current_pid()
{
#ifdef WIN32
return GetCurrentProcessId();
#else
return ::getpid();
#endif
}
}
// int = DOWNLOAD ID; string = file path
wxDEFINE_EVENT(EVT_DWNLDR_FILE_COMPLETE, wxCommandEvent);
// int = DOWNLOAD ID; string = error msg
@@ -133,10 +144,25 @@ void FileGet::priv::get_perform()
std::string extension;
if (m_written == 0)
{
std::string final_filename;
bool found = false;
boost::filesystem::path dest_path = m_dest_folder / m_filename;
extension = dest_path.extension().string();
std::string just_filename = m_filename.substr(0, m_filename.size() - extension.size());
std::string final_filename = just_filename;
// Find unsed filename
try {
found = find_unused_filename(m_dest_folder, m_filename, m_tmp_path, final_filename);
size_t version = 0;
while (boost::filesystem::exists(m_dest_folder / (final_filename + extension)) || boost::filesystem::exists(m_dest_folder / (final_filename + extension + "." + std::to_string(get_current_pid()) + ".download")))
{
++version;
if (version > 999) {
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / (final_filename + extension)).string()));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
}
final_filename = GUI::format("%1%(%2%)", just_filename, std::to_string(version));
}
} catch (const boost::filesystem::filesystem_error& e)
{
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
@@ -145,18 +171,10 @@ void FileGet::priv::get_perform()
m_evt_handler->QueueEvent(evt);
return;
}
if (!found) {
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_ERROR);
evt->SetString(GUI::format_wxstr(L"Failed to find suitable filename. Last name: %1%." , (m_dest_folder / final_filename).string()));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
return;
}
m_filename = final_filename;
extension = boost::filesystem::path(m_filename).extension().string();
m_filename = sanitize_filename(final_filename + extension);
m_tmp_path = download_marker_path(m_dest_folder, m_filename);
m_tmp_path = m_dest_folder / (m_filename + "." + std::to_string(get_current_pid()) + ".download");
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
evt->SetString(boost::nowide::widen(m_filename));
@@ -203,32 +221,7 @@ void FileGet::priv::get_perform()
if(dest_path.empty()) {
std::string filename = extract_remote_filename(header);
if (!filename.empty()) {
// The name comes from the server: keep it inside the destination folder and never
// replace an existing file. Keep the current name if nothing usable remains.
filename = sanitize_file_basename(filename);
std::string unused;
try {
if (filename.empty() || !find_unused_filename(m_dest_folder, filename, m_tmp_path, unused))
unused.clear();
} catch (const boost::filesystem::filesystem_error&) {
unused.clear();
}
const boost::filesystem::path tmp_path = unused.empty() ? m_tmp_path : download_marker_path(m_dest_folder, unused);
if (tmp_path != m_tmp_path) {
// Move the marker to the adopted name so that other downloads see the name as taken.
// Only before anything is written, so that no downloaded data has to be carried over.
FILE* tmp_file = m_written == 0 ? fopen(wxString(tmp_path.wstring()).c_str(), "wb") : nullptr;
if (tmp_file != nullptr) {
fclose(file);
boost::system::error_code ec;
boost::filesystem::remove(m_tmp_path, ec);
file = tmp_file;
m_tmp_path = tmp_path;
} else
unused.clear();
}
if (!unused.empty())
m_filename = unused;
m_filename = filename;
dest_path = m_dest_folder / m_filename;
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
evt->SetString(boost::nowide::widen(m_filename));
@@ -334,18 +327,6 @@ void FileGet::priv::get_perform()
m_evt_handler->QueueEvent(evt);
}
fclose(file);
// Another file may have taken the name while downloading.
if (!dest_path.empty() && boost::filesystem::exists(dest_path)) {
std::string unused;
if (!find_unused_filename(m_dest_folder, m_filename, m_tmp_path, unused))
throw std::runtime_error("No unused file name.");
m_filename = unused;
dest_path = m_dest_folder / m_filename;
wxCommandEvent* evt = new wxCommandEvent(EVT_DWNLDR_FILE_NAME_CHANGE);
evt->SetString(boost::nowide::widen(m_filename));
evt->SetInt(m_id);
m_evt_handler->QueueEvent(evt);
}
boost::filesystem::rename(m_tmp_path, dest_path);
}
catch (const std::exception& /*e*/)
+1 -1
View File
@@ -286,7 +286,7 @@ bool OpenGLManager::init_gl(bool popup_error)
bool valid_version = s_gl_info.is_version_greater_or_equal_to(2, 0);
if (!valid_version) {
BOOST_LOG_TRIVIAL(error) << "Found opengl version < 2.0"<< std::endl;
BOOST_LOG_TRIVIAL(error) << "Found opengl version <= 3.2"<< std::endl;
// Complain about the OpenGL version.
if (popup_error) {
wxString message = from_u8((boost::format(
+47 -38
View File
@@ -15909,11 +15909,6 @@ void Plater::import_model_id(wxString download_info)
//wxString sError = error.what();
}
// The name comes from the link: reduce it to a plain file name inside the download folder.
filename = from_u8(sanitize_file_basename(into_u8(filename)));
if (filename.empty())
filename = "untitled.3mf";
bool download_ok = false;
int retry_count = 0;
const int max_retries = 3;
@@ -15955,28 +15950,51 @@ void Plater::import_model_id(wxString download_info)
msg = _L("Preparing 3MF file...");
//gets the number of files with the same name
std::vector<wxString> vecFiles;
bool is_already_exist = false;
target_path = fs::path(wxGetApp().app_config->get("download_path"));
//check file suffix
wxString extension = fs::path(filename.wx_str()).extension().c_str();
if (!extension.Contains(".3mf")) {
msg = _L("Download failed; unknown file format.");
return;
try
{
vecFiles.clear();
wxString extension = fs::path(filename.wx_str()).extension().c_str();
//check file suffix
if (!extension.Contains(".3mf")) {
msg = _L("Download failed; unknown file format.");
return;
}
auto name = filename.substr(0, filename.length() - extension.length() - 1);
for (const auto& iter : boost::filesystem::directory_iterator(target_path))
{
if (boost::filesystem::is_directory(iter.path()))
continue;
wxString sFile = iter.path().filename().string().c_str();
if (strstr(sFile.c_str(), name.c_str()) != NULL) {
vecFiles.push_back(sFile);
}
if (sFile == filename) is_already_exist = true;
}
}
catch (const std::exception&)
{
//wxString sError = error.what();
}
//never replace an existing file
std::string unused_filename;
try {
if (!find_unused_filename(target_path, into_u8(filename), {}, unused_filename))
unused_filename.clear();
} catch (const std::exception&) {
unused_filename.clear();
//update filename
if (is_already_exist && vecFiles.size() >= 1) {
wxString extension = fs::path(filename.wx_str()).extension().c_str();
wxString name = filename.substr(0, filename.length() - extension.length());
filename = wxString::Format("%s(%d)%s", name, vecFiles.size() + 1, extension).ToStdString();
}
if (unused_filename.empty()) {
msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it.");
return;
}
filename = from_u8(unused_filename);
msg = _L("Downloading project...");
@@ -15988,6 +16006,10 @@ void Plater::import_model_id(wxString download_info)
boost::uuids::uuid uuid = boost::uuids::random_generator()();
std::string unique = to_string(uuid).substr(0, 6);
if (filename.empty()) {
filename = "untitled.3mf";
}
//target_path /= (boost::format("%1%_%2%.3mf") % filename % unique).str();
target_path /= fs::path(filename.wc_str());
@@ -16036,26 +16058,13 @@ void Plater::import_model_id(wxString download_info)
cont = false;
}
})
.on_complete([&cont, &download_ok, &msg, tmp_path, &target_path](std::string body, unsigned /* http_status */) {
.on_complete([&cont, &download_ok, tmp_path, target_path](std::string body, unsigned /* http_status */) {
fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
file.write(body.c_str(), body.size());
file.close();
fs::rename(tmp_path, target_path);
cont = false;
try {
// Another file may have taken the name while downloading.
std::string unused_filename;
if (find_unused_filename(target_path.parent_path(), target_path.filename().string(), {}, unused_filename)) {
target_path = target_path.parent_path() / unused_filename;
fs::rename(tmp_path, target_path);
download_ok = true;
return;
}
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(error) << "import_model_id: failed to move the download into place: " << e.what();
}
boost::system::error_code ec;
fs::remove(tmp_path, ec);
msg = _L("Importing to Orca Slicer failed. Please download the file and manually import it.");
download_ok = true;
}).perform_sync();
// for break while
-58
View File
@@ -1199,64 +1199,6 @@ TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one li
REQUIRE(single_smooth.length == single_sharp.length);
}
TEST_CASE("Multiline infill of an object matches the infill of a larger object with the same center", "[Fill]")
{
const InfillPattern pattern = GENERATE(ipGrid, ipTriangles, ipStars, ipCubic);
const int multiline = GENERATE(2, 3);
// A square with cells as large as itself, whose corners are as far out as the object bounding box
// reaches, and a strip with small cells, whose extents change with every layer orientation.
const auto [half, density] = GENERATE(table<Vec2d, float>({ { Vec2d(20., 20.), 0.15f }, { Vec2d(60., 4.), 0.35f } }));
CAPTURE(pattern, multiline, half.x(), half.y(), density);
// Off the origin; the same center gives both objects the same pattern.
auto rectangle = [](const Vec2d &half) {
const Vec2d center(100., 60.);
return ExPolygon{ Points{ Point::new_scale(center.x() - half.x(), center.y() - half.y()),
Point::new_scale(center.x() + half.x(), center.y() - half.y()),
Point::new_scale(center.x() + half.x(), center.y() + half.y()),
Point::new_scale(center.x() - half.x(), center.y() + half.y()) } };
};
const ExPolygon object = rectangle(half);
const ExPolygon larger = rectangle(half + Vec2d(10., 10.));
auto fill = [pattern, multiline, density = density](const ExPolygon &region, size_t layer_id) {
std::unique_ptr<Fill> filler(Fill::new_from_type(pattern));
filler->spacing = 0.45;
filler->angle = float(M_PI / 7.);
filler->fixed_angle = true;
filler->layer_id = layer_id;
filler->z = 0.2 * double(layer_id + 1);
filler->set_bounding_box(get_extents(region.contour));
FillParams params;
params.density = density;
params.multiline = multiline;
params.dont_adjust = true;
Surface surface(stInternal, region);
return filler->fill_surface(&surface, params);
};
// Away from the boundary of the object, where both are clipped and connected the same way.
const Polygons inner = shrink(to_polygons(object), scale_(1.));
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
double distance = 0.;
for (const Polyline &path : intersection_pl(from, inner))
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
distance = std::max(distance, tree.distance_from_lines<false>(point));
return unscale<double>(distance);
};
// Both layer orientations of Grid, all three of the triangular family.
for (size_t layer_id = 0; layer_id < 3; ++layer_id) {
CAPTURE(layer_id);
const Polylines walls = fill(object, layer_id);
REQUIRE_FALSE(walls.empty());
CHECK(get_intersections(to_lines(walls)).empty());
const Polylines reference = fill(larger, layer_id);
CHECK(farthest(reference, walls) < 0.01);
CHECK(farthest(walls, reference) < 0.01);
}
}
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
{
const int multiline = GENERATE(2, 3);
+15 -208
View File
@@ -17,10 +17,8 @@
#include <nlohmann/json.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <functional>
#include <catch2/catch_tostring.hpp>
#include <Eigen/Core>
@@ -186,13 +184,16 @@ static bool read_cad_recipe_entry(const std::string& path, std::string& out,
return found;
}
// Rewrites the archive at `path`, letting `edit` change the name or data of each entry; returns
// whether `edit` reported a change for any of them. miniz cannot edit in place and
// open_zip_writer truncates, so the entries are held across the switch.
static bool rewrite_3mf_entries(const std::string& path, const std::function<bool(std::string& name, std::string& data)>& edit)
// Rewrites the archive at `path` with the recipe entry back under the name it had before the
// rename, which is what every project saved by an earlier build looks like on disk. Generated
// rather than checked in because a whole project archive is not frozen evidence the way a bare
// recipe blob is -- it has to be whatever today's exporter writes, with only the name aged.
// miniz cannot rename in place and open_zip_writer truncates, so the entries are held across
// the switch.
static void rename_cad_recipe_entry_to_legacy(const std::string& path)
{
std::vector<std::pair<std::string, std::string>> entries;
bool changed = false;
bool renamed = false;
{
mz_zip_archive zip;
mz_zip_zero_struct(&zip);
@@ -207,140 +208,22 @@ static bool rewrite_3mf_entries(const std::string& path, const std::function<boo
std::string data((size_t) st.m_uncomp_size, '\0');
if (st.m_uncomp_size > 0)
REQUIRE(mz_zip_reader_extract_to_mem(&zip, i, data.data(), data.size(), 0));
changed |= edit(name, data);
if (boost::algorithm::iequals(name, CAD_RECIPE_ENTRY)) {
name = LEGACY_CAD_RECIPE_ENTRY;
renamed = true;
}
entries.emplace_back(std::move(name), std::move(data));
}
close_zip_reader(&zip);
}
// Without this the scenario would degrade silently into re-testing the new name if the
// exporter's constant ever moved again: every load below would still pass.
REQUIRE(renamed);
Zipper out(path);
for (const auto& e : entries)
out.add_entry(e.first, e.second.data(), e.second.size());
out.finalize();
return changed;
}
// Rewrites the archive at `path` with the recipe entry back under the name it had before the
// rename, which is what every project saved by an earlier build looks like on disk. Generated
// rather than checked in because a whole project archive is not frozen evidence the way a bare
// recipe blob is -- it has to be whatever today's exporter writes, with only the name aged.
static void rename_cad_recipe_entry_to_legacy(const std::string& path)
{
const bool renamed = rewrite_3mf_entries(path, [](std::string& name, std::string&) {
if (!boost::algorithm::iequals(name, CAD_RECIPE_ENTRY))
return false;
name = LEGACY_CAD_RECIPE_ENTRY;
return true;
});
// Without this the scenario would degrade silently into re-testing the new name if the
// exporter's constant ever moved again: every load below would still pass.
REQUIRE(renamed);
}
// Replaces the first occurrence of `from` in any entry whose name ends with `suffix`.
static bool replace_in_3mf_entry(const std::string& path, const std::string& suffix, const std::string& from, const std::string& to)
{
bool replaced = false;
rewrite_3mf_entries(path, [&](std::string& name, std::string& data) {
if (replaced || !boost::algorithm::ends_with(name, suffix))
return false;
if (const size_t pos = data.find(from); pos != std::string::npos) {
data.replace(pos, from.size(), to);
replaced = true;
}
return replaced;
});
return replaced;
}
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
static void store_painted_cube(const std::string& path)
{
Model model;
ModelObject* object = model.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
{
TriangleSelector selector(volume->mesh());
selector.set_facet(0, EnforcerBlockerType::Extruder2);
selector.set_facet(1, EnforcerBlockerType::Extruder3);
REQUIRE(volume->mmu_segmentation_facets.set(selector));
}
ScopedTemporaryDir backup_dir("orca_paint_src");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig cfg;
PlateData plate;
plate.plate_index = 0;
StoreParams sp;
sp.path = path.c_str();
sp.model = &model;
sp.config = &cfg;
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
sp.plate_data_list.push_back(&plate);
REQUIRE(store_bbs_3mf(sp));
}
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
// importer stages metadata through `backup_dir`, which has to outlive the model.
static bool load_project(const std::string& path, Model& model, const ScopedTemporaryDir& backup_dir)
{
model.set_backup_path(backup_dir.string());
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
PlateDataPtrs plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
const bool loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates, &project_presets, &is_bbl_3mf,
&is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
release_PlateData_list(plates);
return loaded;
}
TEST_CASE("A project with a plate id below 1 fails to load", "[3mf][Regression]")
{
const int plate_id = GENERATE(0, -1);
INFO("plater_id " << plate_id);
ScopedTemporaryFile temp(".3mf");
store_painted_cube(temp.string());
{
ScopedTemporaryDir backup_dir("orca_plate_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
}
REQUIRE(replace_in_3mf_entry(temp.string(), "model_settings.config", "key=\"plater_id\" value=\"1\"",
"key=\"plater_id\" value=\"" + std::to_string(plate_id) + "\""));
ScopedTemporaryDir backup_dir("orca_plate_dst");
Model model;
bool loaded = true;
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
REQUIRE_FALSE(loaded);
}
TEST_CASE("A project with malformed paint data loads without the damaged facet", "[3mf][Regression]")
{
ScopedTemporaryFile temp(".3mf");
store_painted_cube(temp.string());
// Split codes with no children behind them: the stream runs out mid-tree.
REQUIRE(replace_in_3mf_entry(temp.string(), ".model", "paint_color=\"8\"", "paint_color=\"FFFFFFFFFFFFFFFF3\""));
ScopedTemporaryDir backup_dir("orca_paint_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const ModelVolume& volume = *model.objects.front()->volumes.front();
const auto& data = volume.mmu_segmentation_facets.get_data();
REQUIRE_FALSE(data.used_states[size_t(EnforcerBlockerType::Extruder2)]);
REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder3)]);
TriangleSelector selector(volume.mesh());
REQUIRE_NOTHROW(selector.deserialize(data));
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder2) == 0);
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
}
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
@@ -1578,79 +1461,3 @@ SCENARIO("bbs_3mf_is_published detects only genuinely published 3MFs", "[3mf]")
}
}
// Writes a single-entry zip whose central directory carries a zip64 record declaring an
// uncompressed size beyond what the 32-bit expat buffer API can take, while the deflated
// payload inflates to only ~64 KiB. Built by hand because miniz never writes a size that
// disagrees with the data.
static void write_zip_with_oversized_entry(const std::string& path, const std::string& entry)
{
const std::string xml = "<?xml version=\"1.0\"?><!--" + std::string(65536, 'A') + "--><a/>";
size_t comp_len = 0;
void* comp = tdefl_compress_mem_to_heap(xml.data(), xml.size(), &comp_len, TDEFL_DEFAULT_MAX_PROBES);
REQUIRE(comp != nullptr);
const std::string deflated(static_cast<const char*>(comp), comp_len);
mz_free(comp);
const uint32_t crc = static_cast<uint32_t>(mz_crc32(MZ_CRC32_INIT, reinterpret_cast<const unsigned char*>(xml.data()), xml.size()));
const uint64_t claimed_size = (uint64_t(1) << 32) + 16;
std::string out;
auto put = [&out](uint64_t v, int bytes) {
for (int i = 0; i < bytes; ++i)
out.push_back(static_cast<char>((v >> (8 * i)) & 0xFF));
};
// local file header, with the true sizes
put(0x04034b50, 4); put(45, 2); put(0, 2); put(8, 2); put(0, 2); put(0, 2);
put(crc, 4); put(deflated.size(), 4); put(xml.size(), 4); put(entry.size(), 2); put(0, 2);
out += entry + deflated;
// central directory header, sizes deferred to the zip64 extra field
const size_t cd_offset = out.size();
put(0x02014b50, 4); put(45, 2); put(45, 2); put(0, 2); put(8, 2); put(0, 2); put(0, 2);
put(crc, 4); put(0xFFFFFFFF, 4); put(0xFFFFFFFF, 4); put(entry.size(), 2); put(20, 2);
put(0, 2); put(0, 2); put(0, 2); put(0, 4); put(0, 4);
out += entry;
put(0x0001, 2); put(16, 2); put(claimed_size, 8); put(deflated.size(), 8);
const size_t cd_size = out.size() - cd_offset;
// end of central directory
put(0x06054b50, 4); put(0, 2); put(0, 2); put(1, 2); put(1, 2);
put(cd_size, 4); put(cd_offset, 4); put(0, 2);
boost::nowide::ofstream f(path, std::ios::binary);
REQUIRE(f.good());
f.write(out.data(), static_cast<std::streamsize>(out.size()));
REQUIRE(f.good());
}
TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[3mf]") {
ScopedTemporaryFile temp(".3mf");
const std::string path = temp.string();
SECTION("BBS importer") {
write_zip_with_oversized_entry(path, "_rels/.rels");
Model model;
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ForwardCompatibilitySubstitutionRule::Enable};
PlateDataPtrs plates;
std::vector<Preset*> project_presets;
bool is_bbl_3mf = false, is_orca_3mf = false;
Semver file_version;
bool loaded = true;
REQUIRE_NOTHROW(loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates, &project_presets, &is_bbl_3mf,
&is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig));
CHECK_FALSE(loaded);
release_PlateData_list(plates);
}
SECTION("PrusaSlicer importer") {
write_zip_with_oversized_entry(path, "Metadata/Slic3r_PE_model.config");
Model model;
DynamicPrintConfig config;
ConfigSubstitutionContext ctxt{ForwardCompatibilitySubstitutionRule::Disable};
bool loaded = true;
REQUIRE_NOTHROW(loaded = load_3mf(path.c_str(), config, ctxt, &model, false));
CHECK_FALSE(loaded);
}
SECTION("PrusaSlicer fingerprint probe") {
write_zip_with_oversized_entry(path, "3D/3dmodel.model");
PrusaFileParser parser;
CHECK_FALSE(parser.check_3mf_from_prusa(path));
}
}
@@ -356,3 +356,38 @@ SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderPar
REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0));
}
}
SCENARIO("Placeholder parser names in branches that are not taken", "[PlaceholderParser]") {
PlaceholderParser parser;
auto config = DynamicPrintConfig::full_print_config();
parser.apply_config(config);
parser.set("idx", 0);
PlaceholderParser::ContextData context;
context.global_config = std::make_unique<DynamicConfig>();
auto process = [&parser, &context](const std::string &templ) { return parser.process(templ, 0, nullptr, nullptr, &context); };
SECTION("a declaration continuing after a variable reference parses when not taken") {
REQUIRE(process("{if false}{local a = layer_height + 1}{endif}ok") == "ok");
}
SECTION("names are not checked by default") {
REQUIRE(process("{if false}{no_such_var}[no_such_var]{endif}ok") == "ok");
}
SECTION("names must resolve when check_inactive_branches is set") {
struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore;
PlaceholderParser::check_inactive_branches = true;
CHECK_THROWS_WITH(process("{if false}{no_such_var}{endif}"), Catch::Matchers::ContainsSubstring("Not a variable name (in an inactive branch)"));
CHECK_THROWS_WITH(process("{if false}[no_such_var]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)"));
CHECK_THROWS_WITH(process("{if false}[nozzle_temperature[no_such_var]]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)"));
CHECK_THROWS(process("{if true}{else}{no_such_var}{endif}"));
CHECK_THROWS(process("{if false}{local a = no_such_var + 1}{endif}"));
CHECK(process("{if false}{layer_height}[layer_height][nozzle_temperature_0][nozzle_temperature[idx]]{endif}ok") == "ok");
CHECK(process("{if false}{local a = 1}{a = a + 1}{a}{endif}{if false}{a}{endif}ok") == "ok");
// A global declared in a branch that is not taken counts as defined for later expansions sharing the context.
CHECK(process("{if false}{global g = 1}{endif}{if false}{g}{endif}ok") == "ok");
CHECK(process("{if false}{g}{endif}ok") == "ok");
// Boolean expressions are not checked, so compatibility conditions keep their behaviour.
CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config));
}
}
@@ -3,8 +3,6 @@
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <algorithm>
using namespace Slic3r;
// A sphere gives well over ExtruderMax original facets, so every extruder state can be assigned
@@ -125,77 +123,3 @@ TEST_CASE("Extruder states match the CONST_FILAMENTS hex encoding", "[TriangleSe
INFO("Hex " << c.hex << " -> extruder " << c.state);
REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType(c.state)));
}
// Pack 4-bit codes into a bitstream, least significant bit first, in the order the decoder reads them.
static std::vector<bool> pack_nibbles(const std::vector<int> &nibbles)
{
std::vector<bool> bitstream;
for (const int nibble : nibbles)
for (int bit = 0; bit < 4; ++bit)
bitstream.push_back((nibble >> bit) & 1);
return bitstream;
}
TEST_CASE("A valid paint stream with nested splits round-trips bit for bit", "[TriangleSelector]")
{
const TriangleMesh mesh = test_mesh();
TriangleSelector::TriangleSplittingData data;
data.triangles_to_split.emplace_back(0, 0);
// A three-side split whose children, in stream order, are: a one-side split (side 2) into two
// leaves, a two-side split (side 1) into leaves of states 20, 0 and 8, then two plain leaves.
const std::vector<int> triangle_0 = {0b0011,
0b1001, 0b1000, 0b0100,
0b0110, 0b1100, 0b1111, 20 - 18, 0b0000, 0b1100, 8 - 3,
0b1000,
0b0100};
data.bitstream = pack_nibbles(triangle_0);
data.triangles_to_split.emplace_back(5, int(data.bitstream.size()));
const std::vector<bool> triangle_5 = pack_nibbles({0b1100, 3 - 3});
data.bitstream.insert(data.bitstream.end(), triangle_5.begin(), triangle_5.end());
data.reset_used_states();
REQUIRE(data.update_used_states(0));
TriangleSelector restored(mesh);
restored.deserialize(data);
REQUIRE(restored.num_facets(EnforcerBlockerType::Extruder20) == 1);
REQUIRE(restored.num_facets(EnforcerBlockerType::Extruder3) == 1);
REQUIRE(restored.serialize() == data);
}
TEST_CASE("A truncated or malformed paint stream drops only the damaged triangle", "[TriangleSelector][Regression]")
{
struct Case { const char *name; std::vector<int> nibbles; };
const auto c = GENERATE(values<Case>({
{"three-side split missing two children", {0b0011, 0b1000, 0b1000}},
{"leaf missing its state nibble", {0b1100}},
{"leaf missing its second state nibble", {0b1100, 0b1111}},
{"splits nested past the end", {0b0011, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF,
0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF}},
{"one-side split of the nonexistent side 3", {0b1101, 0b1000, 0b1000}},
}));
INFO(c.name);
const TriangleMesh mesh = test_mesh();
TriangleSelector intact(mesh);
intact.set_facet(0, EnforcerBlockerType::Extruder2);
// Triangle 0 stays intact, triangle 1 carries the damaged stream.
TriangleSelector::TriangleSplittingData data = intact.serialize();
data.triangles_to_split.emplace_back(1, int(data.bitstream.size()));
const std::vector<bool> damaged = pack_nibbles(c.nibbles);
data.bitstream.insert(data.bitstream.end(), damaged.begin(), damaged.end());
TriangleSelector restored(mesh);
REQUIRE_NOTHROW(restored.deserialize(data));
// Triangle 1 unwinds completely, so the selector holds exactly the intact paint.
REQUIRE(restored.serialize() == intact.serialize());
REQUIRE_NOTHROW(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder3));
TriangleSelector::TriangleSplittingData recomputed = data;
recomputed.reset_used_states();
REQUIRE_FALSE(recomputed.update_used_states(0));
REQUIRE(std::none_of(recomputed.used_states.begin(), recomputed.used_states.end(), [](bool used) { return used; }));
}
-91
View File
@@ -152,94 +152,3 @@ TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed uncha
REQUIRE(resolve_cli_input_path("").empty());
}
}
TEST_CASE("sanitize_file_basename keeps only a plain file name from an untrusted name", "[Utils]") {
const std::string unicode = "\xe6\xa8\xa1\xe5\x9e\x8b \xc3\xa9t\xc3\xa9.3mf"; // UTF-8 CJK and accented Latin
const auto [input, expected] = GENERATE_COPY(table<std::string, std::string>({
{"normal.3mf", "normal.3mf"},
{"../../x.3mf", "x.3mf"},
{"..\\..\\x.3mf", "x.3mf"},
{"C:\\x.3mf", "x.3mf"},
{"C:x.3mf", "C_x.3mf"},
{"/etc/x", "x"},
{"a/b\\c.gcode", "c.gcode"},
{"x:stream", "x_stream"}, // no NTFS alternate data stream
{"x.", "x."},
{".3mf", ".3mf"},
{unicode, unicode},
}));
CAPTURE(input);
CHECK(sanitize_file_basename(input) == expected);
}
TEST_CASE("sanitize_file_basename rejects names that do not name a file", "[Utils]") {
const std::string input = GENERATE(as<std::string>{}, "", ".", "..", "../..", "dir/", "..\\", " ", ". .", "...");
CAPTURE(input);
CHECK(sanitize_file_basename(input).empty());
}
namespace {
void touch(const boost::filesystem::path &path) { std::ofstream(path.string()) << "existing"; }
std::string file_contents(const boost::filesystem::path &path)
{
std::ifstream file(path.string());
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
}
} // namespace
TEST_CASE("find_unused_filename keeps a name nothing uses", "[Utils]") {
ScopedTemporaryDir dir;
std::string name;
REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name));
CHECK(name == "model.3mf");
}
TEST_CASE("find_unused_filename versions a name an existing file uses", "[Utils]") {
ScopedTemporaryDir dir;
touch(dir.path() / "model.3mf");
std::string name;
REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name));
CHECK(name == "model(1).3mf");
}
TEST_CASE("find_unused_filename versions a name that maps onto an existing file once sanitized", "[Utils]") {
ScopedTemporaryDir dir;
touch(dir.path() / "my_model.3mf");
const std::string input = GENERATE(as<std::string>{}, "my?model.3mf", "my:model.3mf", "my*model.3mf");
CAPTURE(input);
std::string name;
REQUIRE(find_unused_filename(dir.path(), input, {}, name));
CHECK(name == "my_model(1).3mf");
CHECK(file_contents(dir.path() / "my_model.3mf") == "existing");
}
TEST_CASE("find_unused_filename treats the marker of another download as used", "[Utils]") {
ScopedTemporaryDir dir;
touch(download_marker_path(dir.path(), "model.3mf"));
std::string name;
REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name));
CHECK(name == "model(1).3mf");
}
TEST_CASE("find_unused_filename ignores the marker of the download asking", "[Utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path own_marker = download_marker_path(dir.path(), "model.3mf");
touch(own_marker);
std::string name;
REQUIRE(find_unused_filename(dir.path(), "model.3mf", own_marker, name));
CHECK(name == "model.3mf");
}
TEST_CASE("find_unused_filename gives up after 999 versions", "[Utils]") {
ScopedTemporaryDir dir;
touch(dir.path() / "model.3mf");
for (int version = 1; version < 999; ++version)
touch(dir.path() / ("model(" + std::to_string(version) + ").3mf"));
std::string name;
REQUIRE(find_unused_filename(dir.path(), "model.3mf", {}, name));
CHECK(name == "model(999).3mf");
touch(dir.path() / name);
REQUIRE_FALSE(find_unused_filename(dir.path(), "model.3mf", {}, name));
CHECK(name == "model(999).3mf");
}