mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 19:31:22 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f34f37c27 | ||
|
|
185cfe4323 | ||
|
|
78fb4f767a | ||
|
|
05da6bbcc8 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 ¢er)
|
||||
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
|
||||
|
||||
@@ -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 ¶m
|
||||
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 ¶
|
||||
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 ¶
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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*/)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 ®ion, 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
@@ -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; }));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user