Merge remote-tracking branch 'origin/main' into feature/texture_displacement

This commit is contained in:
ExPikaPaka
2026-09-29 08:36:11 +02:00
6839 changed files with 271406 additions and 287256 deletions
+60 -12
View File
@@ -28,6 +28,7 @@
#include <csignal>
#include <atomic>
#include <new>
#include <optional>
#if defined(__linux__) || defined(__LINUX__)
#include <condition_variable>
@@ -1614,6 +1615,10 @@ int CLI::run(int argc, char **argv)
ConfigOptionBool* allow_rotations_option = m_config.option<ConfigOptionBool>("allow_rotations");
if (allow_rotations_option)
allow_rotations = allow_rotations_option->value;
// Only an explicit --align-to-y-axis overrides the printer-structure default.
std::optional<bool> align_to_y_axis;
if (m_given_option_keys.count("align_to_y_axis") > 0)
align_to_y_axis = m_config.opt_bool("align_to_y_axis");
ConfigOptionBool* skip_modified_gcodes_option = m_config.option<ConfigOptionBool>("skip_modified_gcodes");
if (skip_modified_gcodes_option)
@@ -5298,7 +5303,9 @@ int CLI::run(int argc, char **argv)
arrange_cfg.bed_shrink_x = BED_SHRINK_SEQ_PRINT;
arrange_cfg.bed_shrink_y = BED_SHRINK_SEQ_PRINT;
}
if (auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) {
if (align_to_y_axis.has_value()) {
arrange_cfg.align_to_y_axis = *align_to_y_axis;
} else if (auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) {
arrange_cfg.align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3);
}
@@ -5520,12 +5527,28 @@ int CLI::run(int argc, char **argv)
//add the virtual object into unselect list if has
partplate_list.preprocess_exclude_areas(unselected, enable_wrapping_detect);
if (used_filament_set.size() > 0)
// Filament ids given on the command line size the tower for STL input. A project
// records its filament use per plate, so count there and keep its tower positions.
const int plate_count = partplate_list.get_plate_count();
const bool from_project = used_filament_set.empty();
std::vector<int> plate_filament_counts(plate_count, static_cast<int>(used_filament_set.size()));
if (from_project)
for (int plate_index = 0; plate_index < plate_count; ++plate_index)
plate_filament_counts[plate_index] = static_cast<int>(partplate_list.get_plate(plate_index)->get_extruders_under_cli(true, m_print_config).size());
// A project only gets a tower the slicer will print: the prime tower enabled, and not
// a by-object print unless a smooth timelapse needs it, as the per-plate arrange decides.
const bool project_tower_allowed = m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value &&
(is_smooth_timelapse || !arrange_cfg.is_seq_print);
const auto plate_needs_wipe_tower = [from_project, project_tower_allowed, is_smooth_timelapse](int filament_count) {
if (!from_project)
return filament_count > 0;
return project_tower_allowed && (filament_count > 1 || (filament_count > 0 && is_smooth_timelapse));
};
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
if (plate_needs_wipe_tower(max_filament_count))
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
int extruder_size = used_filament_set.size();
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
// This margin only pre-adjusts the default away from the near edges;
// estimate_wipe_tower_polygon below computes the real clamped position.
@@ -5561,7 +5584,11 @@ int CLI::run(int argc, char **argv)
for (int bedid = 0; bedid < MAX_PLATE_COUNT; bedid++) {
int plate_index_valid = std::min(bedid, plate_count - 1);
if (bedid < plate_count) {
// Overflow beds may receive objects from any plate, so size them for the busiest one.
const int extruder_size = bedid < plate_count ? plate_filament_counts[bedid] : max_filament_count;
if (!plate_needs_wipe_tower(extruder_size))
continue;
if (bedid < plate_count && !from_project) {
wipe_x_option->set_at(&wt_x_opt, plate_index_valid, 0);
wipe_y_option->set_at(&wt_y_opt, plate_index_valid, 0);
}
@@ -5748,7 +5775,9 @@ int CLI::run(int argc, char **argv)
arrange_cfg.bed_shrink_x = BED_SHRINK_SEQ_PRINT;
arrange_cfg.bed_shrink_y = BED_SHRINK_SEQ_PRINT;
}
if (auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) {
if (align_to_y_axis.has_value()) {
arrange_cfg.align_to_y_axis = *align_to_y_axis;
} else if (auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure")) {
arrange_cfg.align_to_y_axis = (printer_structure_opt->value == PrinterStructure::psI3);
}
@@ -7015,6 +7044,12 @@ int CLI::run(int argc, char **argv)
}
}
sliced_info.sliced_plates.push_back(sliced_plate_info);
} catch (const Slic3r::SlicingErrors &exs) {
const std::string message = print_fff ? print_fff->slicing_errors_message(exs) : std::string(exs.what());
BOOST_LOG_TRIVIAL(error) << "found slicing or export error for partplate " << index+1 << ": " << message;
boost::nowide::cerr << message << std::endl;
record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, message, sliced_info);
flush_and_exit(CLI_SLICING_ERROR);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "found slicing or export error for partplate "<<index+1 << std::endl;
boost::nowide::cerr << ex.what() << std::endl;
@@ -7255,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 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;
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;
bool thumbnail_opengl_ready = false;
glfwSetErrorCallback(glfw_callback);
@@ -7269,8 +7304,9 @@ int CLI::run(int argc, char **argv)
}
else {
BOOST_LOG_TRIVIAL(info) << "glfwInit Success."<< std::endl;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, gl_major);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, gl_minor);
// 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_RED_BITS, 8);
glfwWindowHint(GLFW_GREEN_BITS, 8);
glfwWindowHint(GLFW_BLUE_BITS, 8);
@@ -7286,6 +7322,16 @@ 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;
@@ -7943,6 +7989,8 @@ bool CLI::setup(int argc, char **argv)
for (std::string &input_file : m_input_files)
input_file = resolve_cli_input_path(input_file);
m_given_option_keys.insert(opt_order.begin(), opt_order.end());
// Parse actions and transform options.
for (auto const &opt_key : opt_order) {
if (cli_actions_config_def.has(opt_key))
+4
View File
@@ -1,6 +1,8 @@
#ifndef SLIC3R_HPP
#define SLIC3R_HPP
#include <set>
#include "libslic3r/Config.hpp"
#include "libslic3r/Model.hpp"
@@ -113,6 +115,8 @@ private:
std::vector<std::string> m_input_files;
std::vector<std::string> m_actions;
std::vector<std::string> m_transforms;
// Options the user typed; setup() fills the CLI's own options with defaults afterwards.
std::set<std::string> m_given_option_keys;
std::vector<Model> m_models;
bool setup(int argc, char **argv);
+6
View File
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
# profile_include_dump: prints what included templates contribute to a vendor's presets,
# to diff against the same tool built in BambuStudio. Built only on request.
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
target_link_libraries(profile_include_dump libslic3r boost_headeronly)
target_compile_definitions(profile_include_dump PRIVATE ${_DEV_DEFS})
endif()
# Function that adds source file encoding check to a target
+292 -104
View File
@@ -20,6 +20,7 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/log/trivial.hpp>
#include <boost/log/core.hpp>
@@ -32,6 +33,7 @@
#include <algorithm>
#include <fstream>
#include <iostream>
#include <set>
#include <string>
using namespace Slic3r;
@@ -122,17 +124,19 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
// Put the prime tower where the GUI and CLI would before slicing. The config default (x 15, y 220)
// lies off any bed shallower than the tower, and generation rejects an off-plate tower instead of
// exporting it. Beside the centred cube, clear of the edge exclusion strips some beds carry, then
// pulled inside the printable outline by the tower's own estimated footprint, with a few mm of
// clearance so the conflict checker never sees the two touch.
void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
// exporting it. Beside the cube at the bed centre, clear of the edge exclusion strips some beds
// carry, with a few mm of clearance so the conflict checker never sees the two touch. The cube and
// the tower's estimated footprint are then pulled inside the printable outline as one rigid pair:
// moving the tower alone would push it back onto the cube on a narrow bed. Returns that move for
// the cube.
Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
{
const auto *area = cfg.option<ConfigOptionPoints>("printable_area");
if (area == nullptr || area->values.size() < 3)
return;
return Vec2d::Zero();
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(cfg, resolve_wipe_tower_type(cfg), {0, 1}, cfg.opt_float("layer_height"), 10.);
if (footprint.depth < EPSILON)
return;
return Vec2d::Zero();
const double margin = WIPE_TOWER_MARGIN + footprint.brim_width;
// The position is the tower's own origin; a rotated tower extends from it in another
// direction, so place the rotated box's extents rather than the origin.
@@ -143,43 +147,77 @@ void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
const Vec2d size = unscale(local.max) - lo;
Vec2d pos(center.x() + 5. + margin + 5. - lo.x(), center.y() - size.y() / 2. - lo.y());
box.translate(Point::new_scale(pos.x(), pos.y()));
const Vec2f move = WipeTower::move_box_inside_polygon(get_extents(box), Polygons{Polygon::new_scale(area->values)}, scaled<coord_t>(margin));
pos += move.cast<double>();
// A bed too small for the pair keeps the cube at its centre and places the tower alone.
const Polygons bed{Polygon::new_scale(area->values)};
const BoundingBox tower = get_extents(box);
BoundingBox pair = tower;
pair.merge(Point::new_scale(center.x() - 5., center.y() - 5.));
pair.merge(Point::new_scale(center.x() + 5., center.y() + 5.));
const Point room = get_extents(bed).size() - Point::new_scale(2. * margin, 2. * margin);
const bool rigid = pair.size().x() < room.x() && pair.size().y() < room.y();
const Vec2d move = WipeTower::move_box_inside_polygon(rigid ? pair : tower, bed, scaled<coord_t>(margin)).cast<double>();
pos += move;
cfg.option<ConfigOptionFloats>("wipe_tower_x", true)->values = {pos.x()};
cfg.option<ConfigOptionFloats>("wipe_tower_y", true)->values = {pos.y()};
return rigid ? move : Vec2d::Zero();
}
// Slice one centered 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);
place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 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();
@@ -194,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;
}
@@ -253,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;
@@ -312,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);
@@ -336,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;
}
@@ -424,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
+215
View File
@@ -0,0 +1,215 @@
// Prints what the profile `include` key contributes to one vendor's system
// presets, so that the dump from OrcaSlicer and the one from BambuStudio can be
// diffed: the two copies of this file differ only in how they load the vendor.
// For every preset whose values pass through an included template, from its
// own file or an ancestor's, it prints the templates those files name, in the
// order they apply, then the loaded value of each key the templates set. Every
// line starts with the preset, so a plain diff names the preset of each
// difference.
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include "nlohmann/json.hpp"
#include <algorithm>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace Slic3r;
namespace fs = boost::filesystem;
using nlohmann::json;
namespace {
// What a sub-file states about where its values come from, and what it sets.
struct Entry
{
std::string inherits;
std::vector<std::string> includes;
std::vector<std::string> keys;
};
// Sub-files by name, per section of the vendor index: `inherits` and `include`
// both resolve within a section.
using Sections = std::map<std::string, std::map<std::string, Entry>>;
json read_json(const fs::path &path)
{
boost::nowide::ifstream in(path.string());
if (!in)
throw std::runtime_error("Cannot read " + path.string());
return json::parse(in);
}
Sections read_vendor(const fs::path &dir, const std::string &vendor)
{
// Keys that name and place a sub-file rather than set a value.
static const std::set<std::string> metadata = {"name", "type", "from", "instantiation", "inherits", "include"};
Sections sections;
const json index = read_json(dir / (vendor + ".json"));
for (const char *section : {"process_list", "filament_list", "machine_list"}) {
const auto list = index.find(section);
if (list == index.end())
continue;
for (const json &item : *list) {
const json file = read_json(dir / vendor / item.at("sub_path").get<std::string>());
// A file without a name goes by its name in the index.
Entry &entry = sections[section][file.value("name", item.at("name").get<std::string>())];
entry.inherits = file.value("inherits", "");
if (const auto include = file.find("include"); include != file.end()) {
if (include->is_string())
entry.includes.push_back(include->get<std::string>());
else
for (const json &name : *include)
entry.includes.push_back(name.get<std::string>());
}
for (auto it = file.begin(); it != file.end(); ++it)
if (metadata.count(it.key()) == 0)
entry.keys.push_back(it.key());
}
}
return sections;
}
const PresetCollection &presets_of(const PresetBundle &bundle, const std::string &section)
{
if (section == "process_list")
return bundle.prints;
if (section == "filament_list")
return bundle.filaments;
return bundle.printers;
}
size_t dump(const PresetBundle &bundle, const Sections &sections, std::ostream &out)
{
size_t dumped = 0;
for (const auto &[section, entries] : sections) {
std::vector<const Preset *> presets;
for (const Preset &preset : presets_of(bundle, section).get_presets())
if (preset.is_system)
presets.push_back(&preset);
std::sort(presets.begin(), presets.end(), [](const Preset *a, const Preset *b) { return a->name < b->name; });
for (const Preset *preset : presets) {
// The preset and its ancestors, root first: the order their includes apply in.
std::vector<const Entry *> lineage;
for (auto it = entries.find(preset->name); it != entries.end() && lineage.size() <= entries.size();
it = entries.find(it->second.inherits))
lineage.insert(lineage.begin(), &it->second);
std::string templates;
std::set<std::string> keys;
for (const Entry *entry : lineage)
for (const std::string &name : entry->includes) {
templates += (templates.empty() ? "" : "; ") + name;
if (const auto it = entries.find(name); it != entries.end())
keys.insert(it->second.keys.begin(), it->second.keys.end());
}
if (templates.empty())
continue;
const std::string prefix = section.substr(0, section.find('_')) + " | " + preset->name + " | ";
out << prefix << "include = " << templates << "\n";
for (const std::string &key : keys)
out << prefix << key << " = " << (preset->config.has(key) ? preset->config.opt_serialize(key) : "<absent>") << "\n";
++dumped;
}
}
return dumped;
}
// Orca: load the vendor as the app does, against the filament library when the
// directory has one: parsed from its JSON files or, with from_cache, from the
// preset cache the app reads on every launch after the first.
void load_vendor(PresetBundle &bundle, const std::string &dir, const std::string &vendor, bool from_cache)
{
const auto rule = ForwardCompatibilitySubstitutionRule::EnableSilent;
PresetBundle library;
const PresetBundle *base = nullptr;
if (fs::is_regular_file(fs::path(dir) / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json"))) {
library.load_vendor_configs_from_json(dir, PresetBundle::ORCA_FILAMENT_LIBRARY, PresetBundle::LoadSystem, rule, nullptr, false);
base = &library;
}
if (!from_cache) {
bundle.load_vendor_configs_from_json(dir, vendor, PresetBundle::LoadSystem, rule, base, false);
return;
}
// Parse once to write <vendor>.opc beside the profile, then load that
// cache into a clean bundle. Any cache already there goes first, so that a
// failed write cannot leave it to be loaded; one that was not there before
// goes again afterwards.
const fs::path cache = fs::path(dir) / (vendor + ".opc");
const bool had_cache = fs::exists(cache);
fs::remove(cache);
PresetBundle parsed;
parsed.set_is_validation_mode(true); // parse the JSON: validation never serves a cache
parsed.set_generate_vendor_caches(true);
parsed.load_vendor_configs_from_json(dir, vendor, PresetBundle::LoadSystem, rule, base);
const bool loaded = bundle.load_vendor_cache(cache.string(), vendor, parsed.vendors.at(vendor).config_version, base);
if (!had_cache)
fs::remove(cache);
if (!loaded)
throw std::runtime_error("The preset cache " + cache.string() + " was not written or was rejected");
}
} // namespace
int main(int argc, char *argv[])
{
const char *usage = "Usage: profile_include_dump -p <profiles dir> -o <dump file> [-v <vendor>] [-l <log level>] [-c]\n"
" -v vendor to dump, BBL if omitted\n"
" -l log level, 0 (fatal) to 5 (trace); 1 if omitted\n"
" -c load the presets from the vendor's preset cache, generated first, not the JSON\n";
std::string dir, output, vendor = "BBL";
unsigned log_level = 1;
bool from_cache = false;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "-c") {
from_cache = true;
continue;
}
if (i + 1 == argc) {
std::cerr << usage;
return 1;
}
const std::string value = argv[++i];
if (arg == "-p")
dir = value;
else if (arg == "-o")
output = value;
else if (arg == "-v")
vendor = value;
else if (arg == "-l" && value.size() == 1 && value[0] >= '0' && value[0] <= '5')
log_level = unsigned(value[0] - '0');
else {
std::cerr << usage;
return 1;
}
}
if (dir.empty() || output.empty() || !fs::is_directory(dir)) {
std::cerr << usage;
return 1;
}
set_logging_level(log_level);
try {
PresetBundle bundle;
load_vendor(bundle, dir, vendor, from_cache);
boost::nowide::ofstream out(output);
if (!out)
throw std::runtime_error("Cannot write " + output);
const size_t dumped = dump(bundle, read_vendor(dir, vendor), out);
out.close();
if (!out)
throw std::runtime_error("Failed writing " + output);
std::cerr << "Dumped " << dumped << " " << vendor << " presets that include a template\n";
} catch (const std::exception &ex) {
std::cerr << ex.what() << "\n";
return 1;
}
return 0;
}
+9
View File
@@ -224,6 +224,12 @@ void AppConfig::set_defaults()
set("preview_dim_previous_layers_brightness", std::to_string(std::max(0, std::min(brightness, 99))));
}
// ORCA: view type the G-code preview opens with. "auto" keeps the automatic choice (Filament for
// multi material prints, Line Type for single material ones), "last" restores the view type the user
// picked last, any other value is a fixed view type name, see GCodeViewer::view_type_to_config_name().
if (get("preview_default_view_type").empty())
set("preview_default_view_type", "auto");
if (get("filaments_area_preferred_count").empty())
set("filaments_area_preferred_count", "10");
@@ -303,6 +309,9 @@ void AppConfig::set_defaults()
if (get(SETTING_OPENGL_REALISTIC_PHONG).empty())
set_bool(SETTING_OPENGL_REALISTIC_PHONG, true);
if (get(SETTING_OPENGL_REALISTIC_PREVIEW).empty())
set_bool(SETTING_OPENGL_REALISTIC_PREVIEW, false);
if (get(SETTING_OPENGL_SHADING_MODEL).empty())
set(SETTING_OPENGL_SHADING_MODEL, "gouraud");
+1
View File
@@ -42,6 +42,7 @@ using namespace nlohmann;
#define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows"
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals"
#define SETTING_OPENGL_REALISTIC_PREVIEW "opengl_realistic_preview"
#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count"
#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1
+1
View File
@@ -131,6 +131,7 @@ double getadhesionCoeff(const PrintObject* printObject)
}
double adhesionCoeff = 1;
for (const ModelVolume* modelVolume : objectVolumes) {
if (modelVolume->is_precise_seam()) continue; // non-printing helper geometry
for (auto iter = extrudersFirstLayer.begin(); iter != extrudersFirstLayer.end(); iter++) {
if (modelVolume->extruder_id() == *iter) {
if (Model::extruderParamsMap.find(modelVolume->extruder_id()) != Model::extruderParamsMap.end()) {
+2 -2
View File
@@ -2673,10 +2673,10 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
ridge = pipe.Shape();
have_ridge = !ridge.IsNull();
}
} catch (const Standard_Failure&) {
have_ridge = false; // OCCT failure — on OCCT >= 8 Standard_Failure derives from std::exception, so this handler must come first
} catch (const std::exception&) {
have_ridge = false; // fall back to the bare cylinder/bore below
} catch (const Standard_Failure&) {
have_ridge = false; // OCCT failure (not a std::exception) — must be caught here too
}
if (f.thread_internal) {
+2
View File
@@ -15,6 +15,8 @@
#include <TopExp.hxx>
#include <TopTools.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
#include <TopTools_ListOfShape.hxx>
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
#include <Poly_Triangulation.hxx>
#include <gp_Ax2.hxx>
#include <gp_Dir.hxx>
+2
View File
@@ -259,6 +259,8 @@ set(lisbslic3r_sources
GCode/RetractWhenCrossingPerimeters.hpp
GCode/SeamPlacer.cpp
GCode/SeamPlacer.hpp
GCode/PreciseSeam.cpp
GCode/PreciseSeam.hpp
#GCodeSender.cpp
#GCodeSender.hpp
GCode/SmallAreaInfillFlowCompensator.cpp
+3
View File
@@ -950,6 +950,9 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
}
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
}
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
} else {
+6 -3
View File
@@ -697,7 +697,8 @@ public:
}
} else {
// Resize by duplicating the last value.
this->values.resize(n, this->values./*back*/front());
T v = this->values./*back*/front();
this->values.resize(n, v);
}
}
}
@@ -772,8 +773,10 @@ public:
if (this->values.empty())
this->values.resize(rhs_vec->size());
else
this->values.resize(rhs_vec->size(), this->values.front());
else {
T v = this->values.front();
this->values.resize(rhs_vec->size(), v);
}
assert(default_index.size() == rhs_vec->size());
+1
View File
@@ -990,6 +990,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() {
std::vector<std::wstring> font_names;
EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack,
(LPARAM) &font_names);
ReleaseDC(NULL, hDC);
EmbossStyles font_list;
for (const std::wstring &font_name : font_names) {
+79 -2
View File
@@ -423,6 +423,74 @@ bool ExtrusionLoop::is_smooth(double angle_threshold, double min_arm_length) con
return true;
}
// The seam is inserted into the loop unless a vertex lies within the G-code resolution of it, so a
// loop can begin and end with a segment of a few micrometres. A plain loop stops there anyway; a
// scarf extrudes through both ends, and the planner nearly halts on a block that short. Drop the
// vertex next to the seam point instead, so the loop still starts and ends at the seam. A trimmed
// path loses its arc fitting; its geometry is unchanged, it just prints as line segments.
static void trim_seam_ends(ExtrusionPaths &paths, double tolerance)
{
const auto shorter = [tolerance](const Point3 &a, const Point3 &b) { return (b - a).cast<double>().norm() < tolerance; };
while (!paths.empty()) {
Points3 &points = paths.front().polyline.points;
if (points.size() < 2 || !shorter(points[0], points[1]))
break;
if (points.size() > 2) {
points.erase(points.begin() + 1);
paths.front().polyline.fitting_result.clear();
} else if (paths.size() > 1) {
const Point3 seam = points.front();
paths.erase(paths.begin());
paths.front().polyline.points.front() = seam;
paths.front().polyline.fitting_result.clear();
} else {
break;
}
}
while (!paths.empty()) {
Points3 &points = paths.back().polyline.points;
if (points.size() < 2 || !shorter(points[points.size() - 2], points.back()))
break;
if (points.size() > 2) {
points.erase(points.end() - 2);
paths.back().polyline.fitting_result.clear();
} else if (paths.size() > 1) {
const Point3 seam = points.back();
paths.pop_back();
paths.back().polyline.points.back() = seam;
paths.back().polyline.fitting_result.clear();
} else {
break;
}
}
}
// Split `polyline` where the scarf ramp ends. When the split would leave a remainder shorter
// than half a slope step before the next vertex, the ramp is extended to that vertex instead:
// a stub that short makes the motion planner slow down at the end of the ramp. Planners treat
// moves of a millimetre and more as ordinary, so the ramp never grows by more than that.
static void split_at_slope_end(const Polyline3 &polyline, double length, double slope_max_segment_length, Polyline3 &slope, Polyline3 &flat)
{
const double snap_distance = std::min(0.5 * slope_max_segment_length, scale_(1.));
double acc_length = 0.;
size_t line_idx = 0;
for (const Line3 &line : polyline.lines()) {
const double end_length = acc_length + line.length();
if (end_length >= length) {
if (end_length - length < snap_distance) {
polyline.split_at_index(line_idx + 1, &slope, &flat);
return;
}
break;
}
acc_length = end_length;
++line_idx;
}
polyline.split_at_length(length, &slope, &flat);
}
ExtrusionLoopSloped::ExtrusionLoopSloped(ExtrusionPaths& original_paths,
double seam_gap,
double slope_min_length,
@@ -431,6 +499,14 @@ ExtrusionLoopSloped::ExtrusionLoopSloped(ExtrusionPaths& original_paths,
ExtrusionLoopRole role)
: ExtrusionLoop(role)
{
// An eighth of a common line width: the path moves by less than that at the seam.
trim_seam_ends(original_paths, scale_(0.05));
// The caller measured the loop before the trim; a scarf that covers the whole loop must still end at 1.
double trimmed_length = 0.;
for (const ExtrusionPath &path : original_paths)
trimmed_length += unscale_(path.length());
slope_min_length = std::min(slope_min_length, trimmed_length);
// create slopes
const auto add_slop = [this, slope_max_segment_length, seam_gap](const ExtrusionPath &path, const Polyline3 &poly, double ratio_begin, double ratio_end) {
if (poly.empty()) { return; }
@@ -487,12 +563,13 @@ ExtrusionLoopSloped::ExtrusionLoopSloped(ExtrusionPaths& original_paths,
// Split current path into slope and non-slope part
Polyline3 slope_path;
Polyline3 flat_path;
path->polyline.split_at_length(scale_(remaining_length), &slope_path, &flat_path);
split_at_slope_end(path->polyline, scale_(remaining_length), slope_max_segment_length, slope_path, flat_path);
add_slop(*path, slope_path, start_ratio, 1);
start_ratio = 1;
paths.emplace_back(std::move(flat_path), *path);
if (flat_path.size() > 1)
paths.emplace_back(std::move(flat_path), *path);
remaining_length = 0;
} else {
remaining_length -= path_len;
+29 -4
View File
@@ -464,6 +464,16 @@ void group_region_by_fuzzify(PerimeterGenerator& g)
}
}
g.fuzzy_supported_area.reset();
if ((g.has_fuzzy_skin || g.has_fuzzy_hole) && g.lower_slices != nullptr) {
coord_t max_thickness = 0;
for (const auto& region : regions)
if (should_fuzzify(region.config, g.layer_id, 0, true) || should_fuzzify(region.config, g.layer_id, 0, false))
max_thickness = std::max(max_thickness, region.config.thickness);
// Walls farther than a line width plus the noise amplitude from the layer below are bridging; keep them smooth.
g.fuzzy_supported_area = offset_ex(*g.lower_slices, float(g.ext_perimeter_flow.scaled_width() + max_thickness));
}
if (regions.size() == 1) { // optimization
g.regions_by_fuzzify.push_back({regions.front().config, {}});
return;
@@ -560,13 +570,23 @@ static std::vector<MergedFuzzyRegion> collect_merged_fuzzy_regions(const std::ve
return merged_regions;
}
// Afterwards an empty region means nothing to fuzzify, no longer full coverage.
static void restrict_to_supported(std::vector<MergedFuzzyRegion>& merged_regions, const std::optional<ExPolygons>& supported)
{
if (!supported)
return;
for (auto& merged_region : merged_regions)
merged_region.expolygons = merged_region.expolygons.empty() ? *supported : intersection_ex(merged_region.expolygons, *supported);
}
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, const size_t loop_idx, const bool is_contour)
{
Polygon fuzzified;
const auto slice_z = perimeter_generator.slice_z;
const auto& regions = perimeter_generator.regions_by_fuzzify;
if (regions.size() == 1) { // optimization
const auto& supported = perimeter_generator.fuzzy_supported_area;
if (regions.size() == 1 && !supported) { // optimization
const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour);
if (!fuzzify) {
@@ -590,7 +610,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
// Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1) {
const auto& mr = merged_regions.front();
if (mr.expolygons.empty()) {
if (mr.expolygons.empty() && !supported) {
fuzzified = polygon;
fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config);
return fuzzified;
@@ -626,6 +646,8 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty())
merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons);
restrict_to_supported(merged_regions, supported);
// Split the loops into lines with different config, and fuzzy them separately
fuzzified = polygon;
for (const auto& r : merged_regions) {
@@ -689,7 +711,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
const auto slice_z = perimeter_generator.slice_z;
const auto layer_height = perimeter_generator.layer_height;
const auto& regions = perimeter_generator.regions_by_fuzzify;
if (regions.size() == 1) { // optimization
const auto& supported = perimeter_generator.fuzzy_supported_area;
if (regions.size() == 1 && !supported) { // optimization
const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
if (fuzzify)
@@ -703,7 +726,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
if (!merged_regions.empty()) {
// Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty() && !supported) {
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed);
return;
}
@@ -753,6 +776,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty())
merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons);
restrict_to_supported(merged_regions, supported);
// Split the loops into lines with different config, and fuzzy them separately
for (const auto& r : merged_regions) {
const auto splitted = Algorithm::split_line(*extrusion, r.expolygons, false);
+572
View File
@@ -1,3 +1,4 @@
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp"
#include "../ExPolygon.hpp"
#include "../Surface.hpp"
@@ -14,7 +15,9 @@
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <numeric>
#include <tuple>
// Boost pool: Don't use mutexes to synchronize memory allocation.
#define BOOST_POOL_NO_MT
@@ -1318,6 +1321,564 @@ bool has_no_collinear_lines(const Polylines &polylines)
}
#endif
// Non-crossing centerlines for multiline adaptive cubic, see docs/HLSD/multiline-infill.md.
namespace noncrossing {
// y = x() * x + y() in the frame where the sweep family is horizontal.
using Lin = Vec2d;
static const Vec2d family_dir[3] { Vec2d(1., 0.), Vec2d(0.5, 0.5 * sqrt(3.)), Vec2d(0.5, -0.5 * sqrt(3.)) };
struct SweepLine
{
Vec2d a, b; // a.x() < b.x()
int family;
Lin lin;
std::vector<std::pair<double, int>> junctions; // (x, junction)
};
struct Junction
{
Vec2d p;
std::vector<int> lines;
std::vector<std::pair<int, int>> pairs; // (left, right) line of each path through, bottom-up
std::vector<std::pair<int, int>> bends; // (path, bend) of each pair, -1 where it runs straight
};
struct LevelPath
{
std::vector<Vec2d> verts; // start, bends, end
std::vector<int> lines; // line of each piece
std::vector<int> junctions; // junction of each bend
std::vector<int> turn; // 1 turning up, -1 turning down
std::vector<std::vector<Lin>> cuts;
std::vector<std::pair<Lin, int>> pushes; // (line, bend) keeping a wall away from a neighbour's cut
int start_term { -1 }; // junction where the path stops on another line, or -1
int end_term { -1 };
};
// Moves f onto line c (side 1: from below) wherever c lies beyond it, over the stretches overlapping [w0, w1].
static void clip_profile(std::vector<Vec2d> &f, const Lin &c, int side, double w0, double w1, double lim0, double lim1, bool cut_at_window, bool drop_past_limits)
{
const double r0 = std::max(lim0, f.front().x()), r1 = std::min(lim1, f.back().x());
if (r1 <= r0)
return;
const size_t ia = std::upper_bound(f.begin(), f.end(), r0, [](double x, const Vec2d &p) { return x < p.x(); }) - f.begin();
const size_t ib = std::lower_bound(f.begin() + ia, f.end(), r1, [](const Vec2d &p, double x) { return p.x() < x; }) - f.begin();
auto interpolate = [](const Vec2d &a, const Vec2d &b, double x) { return b.x() > a.x() ? a.y() + (x - a.x()) / (b.x() - a.x()) * (b.y() - a.y()) : b.y(); };
std::vector<Vec2d> local{ Vec2d(r0, interpolate(f[ia - 1], f[ia], r0)) };
local.insert(local.end(), f.begin() + ia, f.begin() + ib);
local.emplace_back(r1, interpolate(f[ib - 1], f[ib], r1));
const double tol = 1.;
auto beyond = [&c, side, tol](const Vec2d &p) { return side * (c.x() * p.x() + c.y() - p.y()) - tol; };
std::vector<std::pair<double, double>> stretches;
auto add = [&stretches](double x0, double x1) {
if (!stretches.empty() && stretches.back().second >= x0)
stretches.back().second = x1;
else
stretches.emplace_back(x0, x1);
};
for (size_t i = 1; i < local.size(); ++i) {
const Vec2d &p = local[i - 1], &q = local[i];
if (q.x() <= p.x())
continue;
const double bp = beyond(p), bq = beyond(q);
if (bp > 0. && bq > 0.)
add(p.x(), q.x());
else if (bp > 0. || bq > 0.) {
const double x = p.x() + bp / (bp - bq) * (q.x() - p.x());
if (bp > 0.)
add(p.x(), x);
else
add(x, q.x());
}
}
std::vector<std::pair<double, double>> keep;
for (auto [x0, x1] : stretches) {
if (x1 < w0 || x0 > w1)
continue;
if (drop_past_limits && ((x0 <= r0 && r0 == lim0) || (x1 >= r1 && r1 == lim1)))
continue;
keep.emplace_back(cut_at_window ? std::max(x0, w0) : x0, cut_at_window ? std::min(x1, w1) : x1);
}
if (keep.empty())
return;
auto y_local = [&](double x) {
size_t i = 1;
while (i + 1 < local.size() && local[i].x() < x)
++i;
return interpolate(local[i - 1], local[i], x);
};
std::vector<Vec2d> out(f.begin(), f.begin() + ia);
auto push = [&out, tol](double x, double y) {
if (out.empty() || x > out.back().x() || std::abs(y - out.back().y()) > 2. * tol)
out.emplace_back(x, y);
};
size_t k = 0;
for (const Vec2d &p : local) {
for (; k < keep.size() && keep[k].second < p.x(); ++k) {
const auto [x0, x1] = keep[k];
push(x0, y_local(x0));
push(x0, c.x() * x0 + c.y());
push(x1, c.x() * x1 + c.y());
push(x1, y_local(x1));
}
if (k < keep.size() && keep[k].first <= p.x() && p.x() <= keep[k].second)
continue;
push(p.x(), p.y());
}
for (; k < keep.size(); ++k) {
const auto [x0, x1] = keep[k];
push(x0, y_local(x0));
push(x0, c.x() * x0 + c.y());
push(x1, c.x() * x1 + c.y());
push(x1, y_local(x1));
}
for (size_t i = ib; i < f.size(); ++i)
push(f[i].x(), f[i].y());
f = std::move(out);
}
static std::vector<Vec2d> path_points(const LevelPath &path, const std::vector<SweepLine> &lines, double reach)
{
std::vector<Vec2d> f = path.verts;
const int nb = int(path.junctions.size());
auto x_of = [&path](int b) { return path.verts[b + 1].x(); };
auto sharp = [&](int b) { return lines[path.lines[b]].family != 0 && lines[path.lines[b + 1]].family != 0; };
// The run of bends turning the same way as bend b, up to the neighbouring bends turning the other way.
auto window = [&](int b) {
int l = b - 1, r = b + 1;
while (l >= 0 && path.turn[l] == path.turn[b])
--l;
while (r < nb && path.turn[r] == path.turn[b])
++r;
return std::make_pair(l >= 0 ? x_of(l) : f.front().x(), r < nb ? x_of(r) : f.back().x());
};
// Sharp bends between the slanted lines go last, so they win at the tip of a small triangle.
for (int b = 0; b < nb; ++b)
if (!sharp(b)) {
const auto [w0, w1] = window(b);
for (const Lin &c : path.cuts[b])
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, false);
}
for (int b = 0; b < nb; ++b)
if (sharp(b))
for (const Lin &c : path.cuts[b])
clip_profile(f, c, path.turn[b], x_of(b), x_of(b), x_of(b) - reach, x_of(b) + reach, false, true);
for (const auto &[c, b] : path.pushes) {
const auto [w0, w1] = window(b);
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, true);
}
std::vector<Vec2d> pts;
for (const Vec2d &p : f) {
while (pts.size() >= 2 && std::abs(cross2(Vec2d(pts.back() - pts[pts.size() - 2]), Vec2d(p - pts.back()))) <=
1e-9 * (pts.back() - pts[pts.size() - 2]).norm() * (p - pts.back()).norm())
pts.pop_back();
pts.push_back(p);
}
return pts;
}
static double polyline_length(const std::vector<Vec2d> &pts)
{
double len = 0.;
for (size_t i = 1; i < pts.size(); ++i)
len += (pts[i] - pts[i - 1]).norm();
return len;
}
// Point at the given distance along pts, and the index of the segment it lies on.
static std::pair<Vec2d, size_t> point_along(const std::vector<Vec2d> &pts, double t)
{
for (size_t i = 1; i < pts.size(); ++i) {
const double len = (pts[i] - pts[i - 1]).norm();
if (t <= len)
return { pts[i - 1] + (len > 0. ? t / len : 0.) * (pts[i] - pts[i - 1]), i };
t -= len;
}
return { pts.back(), pts.size() - 1 };
}
} // namespace noncrossing
Polylines multiline_paths(const Lines &lines_in, double d1, double end_overlap, int sweep, const BoundingBox &cover)
{
using namespace noncrossing;
const double eps = scale_(0.002);
const Eigen::Rotation2Dd to_sweep(-sweep * M_PI / 3.);
const BoundingBoxf box(cover.min.cast<double>(), cover.max.cast<double>());
// Lines in the sweep frame, collinear pieces merged.
struct Piece { double c, s0, s1; };
std::array<std::vector<Piece>, 3> pieces;
for (const Line &line : lines_in) {
Vec2d a = line.a.cast<double>(), b = line.b.cast<double>();
if (!Geometry::liang_barsky_line_clipping(a, b, box) || (b - a).norm() < 10. * eps)
continue;
a = to_sweep * a;
b = to_sweep * b;
const double angle = std::atan2(b.y() - a.y(), b.x() - a.x()) / (M_PI / 3.);
if (std::abs(angle - std::round(angle)) > 0.01)
// Not one of the three families.
return {};
const int f = (int(std::round(angle)) % 3 + 3) % 3;
const Vec2d &d = family_dir[f];
const Vec2d n(-d.y(), d.x());
pieces[f].push_back({ n.dot(a), std::min(d.dot(a), d.dot(b)), std::max(d.dot(a), d.dot(b)) });
}
std::vector<SweepLine> lines;
for (int f = 0; f < 3; ++f) {
std::vector<Piece> &ps = pieces[f];
const Vec2d &d = family_dir[f];
const Vec2d n(-d.y(), d.x());
const double slope = d.y() / d.x();
std::sort(ps.begin(), ps.end(), [](const Piece &l, const Piece &r) { return l.c < r.c; });
for (size_t i = 0; i < ps.size();) {
size_t j = i + 1;
while (j < ps.size() && ps[j].c - ps[i].c < eps)
++j;
std::sort(ps.begin() + i, ps.begin() + j, [](const Piece &l, const Piece &r) { return l.s0 < r.s0; });
double c = 0.;
for (size_t k = i; k < j; ++k)
c += ps[k].c / double(j - i);
double s0 = ps[i].s0, s1 = ps[i].s1;
for (size_t k = i + 1; k <= j; ++k) {
if (k < j && ps[k].s0 <= s1 + eps) {
s1 = std::max(s1, ps[k].s1);
continue;
}
const Vec2d a = s0 * d + c * n;
lines.push_back({ a, s1 * d + c * n, f, Lin(slope, a.y() - slope * a.x()), {} });
if (k < j) {
s0 = ps[k].s0;
s1 = ps[k].s1;
}
}
i = j;
}
}
auto along = [](const SweepLine &l, const Vec2d &p) { return family_dir[l.family].dot(p - l.a); };
auto length = [](const SweepLine &l) { return (l.b - l.a).norm(); };
// Crossings, including the ends of lines stopping on another line.
std::vector<Junction> junctions;
auto detect_junctions = [&]() {
struct Hit { Vec2d p; int i, j; };
std::vector<Hit> hits;
std::vector<int> order(lines.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&lines](int l, int r) { return lines[l].a.x() < lines[r].a.x(); });
for (size_t oi = 0; oi < order.size(); ++oi) {
const SweepLine &li = lines[order[oi]];
for (size_t oj = oi + 1; oj < order.size() && lines[order[oj]].a.x() <= li.b.x() + eps; ++oj) {
const SweepLine &lj = lines[order[oj]];
if (li.family == lj.family)
continue;
const double x = (lj.lin.y() - li.lin.y()) / (li.lin.x() - lj.lin.x());
const Vec2d p(x, li.lin.x() * x + li.lin.y());
const double ti = along(li, p), tj = along(lj, p);
if (ti > -eps && ti < length(li) + eps && tj > -eps && tj < length(lj) + eps)
hits.push_back({ p, order[oi], order[oj] });
}
}
std::sort(hits.begin(), hits.end(), [](const Hit &l, const Hit &r) { return l.p.x() < r.p.x(); });
junctions.clear();
for (const Hit &hit : hits) {
int found = -1;
for (int k = int(junctions.size()) - 1; k >= 0 && junctions[k].p.x() > hit.p.x() - eps; --k)
if (std::abs(junctions[k].p.y() - hit.p.y()) < eps) {
found = k;
break;
}
if (found < 0) {
found = int(junctions.size());
junctions.push_back({ hit.p, {}, {}, {} });
}
std::vector<int> &jl = junctions[found].lines;
for (int li : { hit.i, hit.j })
if (std::find(jl.begin(), jl.end(), li) == jl.end())
jl.push_back(li);
}
for (SweepLine &l : lines)
l.junctions.clear();
for (int ji = 0; ji < int(junctions.size()); ++ji)
for (int li : junctions[ji].lines)
lines[li].junctions.emplace_back(junctions[ji].p.x(), ji);
for (SweepLine &l : lines)
std::sort(l.junctions.begin(), l.junctions.end());
};
detect_junctions();
auto has_arm = [&](int ji, int li, bool right) {
const double t = along(lines[li], junctions[ji].p);
return right ? t < length(lines[li]) - eps : t > eps;
};
// A line ending on another just past a crossing stops at the crossing, where its stub would leave a hole.
auto crosses = [&](int ji, int li) { return has_arm(ji, li, false) && has_arm(ji, li, true); };
for (int pass = 0; pass < 3; ++pass) {
std::vector<bool> touched(lines.size(), false);
bool changed = false;
for (int ji = 0; ji < int(junctions.size()); ++ji) {
const Junction &J = junctions[ji];
if (J.lines.size() != 2 || touched[J.lines[0]] || touched[J.lines[1]] || !crosses(ji, J.lines[0]) || !crosses(ji, J.lines[1]))
continue;
// Shortest arm of each line from J to the junction where it ends on another line.
struct DeadArm { double length; bool at_b; int end; };
std::array<DeadArm, 2> dead;
dead.fill({ std::numeric_limits<double>::max(), false, -1 });
for (int k = 0; k < 2; ++k) {
const SweepLine &l = lines[J.lines[k]];
const size_t at = std::find_if(l.junctions.begin(), l.junctions.end(), [ji](const std::pair<double, int> &j) { return j.second == ji; }) - l.junctions.begin();
const double t = along(l, J.p);
if (at + 1 < l.junctions.size() && length(l) - along(l, junctions[l.junctions[at + 1].second].p) < eps)
dead[k] = { length(l) - t, true, l.junctions[at + 1].second };
if (at > 0 && along(l, junctions[l.junctions[at - 1].second].p) < eps && t < dead[k].length)
dead[k] = { t, false, l.junctions[at - 1].second };
}
const int k = dead[0].length <= dead[1].length ? 0 : 1;
if (dead[k].length >= 2. * d1)
continue;
SweepLine &l = lines[J.lines[k]];
(dead[k].at_b ? l.b : l.a) = J.p;
// Only the shortened line and those it ended on have stale junctions until the next pass.
for (int li : junctions[dead[k].end].lines)
touched[li] = true;
changed = true;
}
if (!changed)
break;
detect_junctions();
}
// At every crossing the lines bounce off each other, so that every path keeps running left to right.
for (int ji = 0; ji < int(junctions.size()); ++ji) {
Junction &J = junctions[ji];
std::vector<int> left, right;
for (int li : J.lines)
if (has_arm(ji, li, false) && has_arm(ji, li, true))
left.push_back(li);
right = left;
std::sort(left.begin(), left.end(), [&lines](int l, int r) { return lines[l].lin.x() > lines[r].lin.x(); });
std::sort(right.begin(), right.end(), [&lines](int l, int r) { return lines[l].lin.x() < lines[r].lin.x(); });
for (size_t k = 0; k < left.size(); ++k)
J.pairs.emplace_back(left[k], right[k]);
J.bends.assign(J.pairs.size(), { -1, -1 });
}
std::vector<LevelPath> paths;
for (int li = 0; li < int(lines.size()); ++li) {
const SweepLine &l = lines[li];
const int start = !l.junctions.empty() && !has_arm(l.junctions.front().second, li, false) ? l.junctions.front().second : -1;
LevelPath path;
path.start_term = start;
path.verts.push_back(start >= 0 ? junctions[start].p : l.a);
path.lines.push_back(li);
int cur = li;
double x = path.verts.front().x();
for (;;) {
const auto &js = lines[cur].junctions;
const auto it = std::find_if(js.begin(), js.end(), [x, eps](const std::pair<double, int> &j) { return j.first > x + 0.25 * eps; });
if (it == js.end()) {
path.verts.push_back(lines[cur].b);
break;
}
Junction &J = junctions[it->second];
const size_t k = std::find_if(J.pairs.begin(), J.pairs.end(), [cur](const std::pair<int, int> &p) { return p.first == cur; }) - J.pairs.begin();
if (k == J.pairs.size()) {
path.verts.push_back(J.p);
path.end_term = it->second;
break;
}
if (const int next = J.pairs[k].second; next != cur) {
J.bends[k] = { int(paths.size()), int(path.junctions.size()) };
path.verts.push_back(J.p);
path.lines.push_back(next);
path.junctions.push_back(it->second);
path.turn.push_back(lines[next].lin.x() > lines[cur].lin.x() ? 1 : -1);
cur = next;
}
x = it->first;
}
path.cuts.resize(path.junctions.size());
paths.push_back(std::move(path));
}
// Cut the two bends of a crossing d1 apart, no further than the neighbouring bends turning the other way.
for (const Junction &J : junctions) {
if (J.pairs.size() < 2 || J.bends.front().first < 0 || J.bends.back().first < 0)
continue;
const Vec2d n = (family_dir[lines[J.pairs.back().second].family] - family_dir[lines[J.pairs.back().first].family]).normalized();
auto cut = [&J, &n](double offset) {
const Vec2d q = J.p + offset * n;
const double s = -n.x() / n.y();
return Lin(s, q.y() - s * q.x());
};
LevelPath &lo = paths[J.bends.front().first], &hi = paths[J.bends.back().first];
const int lb = J.bends.front().second, hb = J.bends.back().second;
if (J.pairs.size() == 3) {
lo.cuts[lb].push_back(cut(-d1));
hi.cuts[hb].push_back(cut(d1));
continue;
}
// Only the tip of a small triangle, between the two slanted families, stops at its neighbouring bends.
const bool sharp = lines[J.pairs.front().first].family != 0 && lines[J.pairs.front().second].family != 0;
auto room = [&](const LevelPath &P, int b, int away, double sign) {
double c = std::numeric_limits<double>::max();
for (int nb : { b - 1, b + 1 })
if (sharp && nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away)
c = std::min(c, sign * n.dot(junctions[P.junctions[nb]].p - J.p));
if (b == 0 && P.start_term >= 0)
c = std::min(c, sign * n.dot(P.verts.front() - J.p));
if (b + 1 == int(P.junctions.size()) && P.end_term >= 0)
c = std::min(c, sign * n.dot(P.verts.back() - J.p));
return std::max(c, 0.);
};
const double c_lo = room(lo, lb, 1, -1.), c_hi = room(hi, hb, -1, 1.);
double d_lo = 0.5 * d1;
if (d1 - c_hi <= c_lo)
d_lo = std::clamp(d_lo, d1 - c_hi, c_lo);
else if (c_lo + c_hi > 0.)
d_lo = d1 * c_lo / (c_lo + c_hi);
const double d_hi = d1 - d_lo;
lo.cuts[lb].push_back(cut(-d_lo));
hi.cuts[hb].push_back(cut(d_hi));
auto propagate = [&](const LevelPath &P, int b, int away, int step, double offset) {
for (int nb : { b - 1, b + 1 })
if (nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away) {
const Junction &W = junctions[P.junctions[nb]];
const int k = int(std::find_if(W.pairs.begin(), W.pairs.end(), [&](const std::pair<int, int> &p) { return p.first == P.lines[nb]; }) - W.pairs.begin()) + step;
if (k >= 0 && k < int(W.bends.size()) && W.bends[k].first >= 0)
paths[W.bends[k].first].pushes.emplace_back(cut(offset), W.bends[k].second);
}
};
if (sharp) {
propagate(lo, lb, 1, -1, -d_lo - d1);
propagate(hi, hb, -1, 1, d_hi + d1);
}
}
std::vector<std::vector<Vec2d>> geometry;
Linesf segments;
std::vector<std::pair<int, double>> segment_start; // path, distance along it
std::vector<std::pair<double, double>> kept; // stretch of each path left by the trimming
for (const LevelPath &path : paths) {
geometry.push_back(path.junctions.empty() ? std::vector<Vec2d>{ path.verts.front(), path.verts.back() } : path_points(path, lines, 4. * d1));
double along_path = 0.;
for (size_t i = 1; i < geometry.back().size(); ++i) {
segments.emplace_back(geometry.back()[i - 1], geometry.back()[i]);
segment_start.emplace_back(int(geometry.size()) - 1, along_path);
along_path += (geometry.back()[i] - geometry.back()[i - 1]).norm();
}
kept.emplace_back(0., along_path);
}
// A path ending on another line stops end_overlap inside the walls of the others, as trimmed so far.
const double end_clearance = d1 - end_overlap;
AABBTreeLines::LinesDistancer<Linef> tree(segments);
auto clearance = [&](int pi, const Vec2d &q) {
double dist = std::numeric_limits<double>::max();
for (size_t s : tree.all_lines_in_radius(q, d1)) {
const auto [pj, start] = segment_start[s];
const Vec2d a = segments[s].a, d = segments[s].b - a;
const double len = d.norm(), t0 = std::max(0., kept[pj].first - start), t1 = std::min(len, kept[pj].second - start);
if (pj != pi && len > 0. && t0 <= t1)
dist = std::min(dist, line_alg::distance_to(Linef(a + t0 / len * d, a + t1 / len * d), q));
}
return dist;
};
// Returns the length trimmed off.
auto trim_front = [&](int pi, std::vector<Vec2d> &pts) {
const double total = polyline_length(pts), step = d1 / 32.;
double t = 0.;
while (t <= total && clearance(pi, point_along(pts, t).first) < end_clearance)
t += step;
if (t > total) {
pts.clear();
return total;
}
if (t == 0.)
return 0.;
for (double lo = std::max(0., t - step); t - lo > step / 256.;)
if (const double mid = 0.5 * (lo + t); clearance(pi, point_along(pts, mid).first) < end_clearance)
lo = mid;
else
t = mid;
const auto [q, seg] = point_along(pts, t);
pts.erase(pts.begin(), pts.begin() + (seg - 1));
pts.front() = q;
return t;
};
// A path stopping on the line of another path is trimmed first, so that it gives way to that path.
std::vector<std::vector<std::tuple<double, double, int>>> carried(lines.size()); // x range and path of each piece
for (int pi = 0; pi < int(paths.size()); ++pi)
for (size_t i = 0; i < paths[pi].lines.size(); ++i)
carried[paths[pi].lines[i]].emplace_back(paths[pi].verts[i].x(), paths[pi].verts[i + 1].x(), pi);
std::vector<std::vector<int>> stopping_on(paths.size());
for (int pi = 0; pi < int(paths.size()); ++pi)
for (const auto &[ji, own] : { std::make_pair(paths[pi].start_term, paths[pi].lines.front()), std::make_pair(paths[pi].end_term, paths[pi].lines.back()) })
if (ji >= 0)
for (int li : junctions[ji].lines)
if (li != own)
for (const auto &[x0, x1, pj] : carried[li])
if (pj != pi && x0 - eps <= junctions[ji].p.x() && junctions[ji].p.x() <= x1 + eps)
stopping_on[pj].push_back(pi);
std::vector<int> order;
std::vector<bool> visited(paths.size(), false);
std::function<void(int)> visit = [&](int pi) {
if (visited[pi])
return;
visited[pi] = true;
for (int child : stopping_on[pi])
visit(child);
order.push_back(pi);
};
for (int pi = 0; pi < int(paths.size()); ++pi)
visit(pi);
std::vector<std::vector<Vec2d>> trimmed(paths.size());
auto trim = [&](int pi) {
std::vector<Vec2d> &pts = trimmed[pi];
if (paths[pi].start_term >= 0)
kept[pi].first += trim_front(pi, pts);
if (paths[pi].end_term >= 0 && !pts.empty()) {
std::reverse(pts.begin(), pts.end());
kept[pi].second -= trim_front(pi, pts);
std::reverse(pts.begin(), pts.end());
}
if (pts.size() < 2 || polyline_length(pts) < d1) {
pts.clear();
kept[pi] = { 0., -1. };
}
};
// Ends grow back where the ends they gave way to were trimmed later; the last pass only shortens them.
for (int pass = 0; pass < 3; ++pass)
for (int pi : order) {
if (pass < 2) {
trimmed[pi] = geometry[pi];
kept[pi] = { 0., polyline_length(geometry[pi]) };
} else if (trimmed[pi].empty())
continue;
trim(pi);
}
Polylines out;
const Eigen::Rotation2Dd to_world = to_sweep.inverse();
for (const std::vector<Vec2d> &pts : trimmed) {
if (pts.empty())
continue;
Polyline pl;
for (const Vec2d &p : pts) {
const Vec2d w = to_world * p;
pl.points.emplace_back(coord_t(std::round(w.x())), coord_t(std::round(w.y())));
}
out.emplace_back(std::move(pl));
}
return out;
}
void Filler::_fill_surface_single(
const FillParams &params,
unsigned int thickness_layers,
@@ -1371,6 +1932,17 @@ void Filler::_fill_surface_single(
all_polylines.reserve(lines.size());
std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; });
if (params.multiline > 1) {
const double d1 = scale_(this->spacing) * params.multiline;
BoundingBox cover = get_extents(expolygon);
cover.offset(coord_t(4. * d1));
// Rotate the family the paths run along with the layer, like the other multiline patterns.
const int sweep = int((this->layer_id / std::max(thickness_layers, 1u)) % 3);
// Line ends overlap the walls they stop on by half a line, so that they bond.
if (Polylines paths = multiline_paths(lines, d1, 0.5 * scale_(this->spacing), sweep, cover); !paths.empty())
all_polylines = std::move(paths);
}
// Apply multiline offset if needed
multiline_fill(all_polylines, params, spacing);
+3
View File
@@ -48,6 +48,9 @@ FillAdaptive::OctreePtr build_octree(
// If true, octree is densified below internal overhangs only.
bool support_overhangs_only);
// Multiline infill: lines of the three families to non-crossing paths d1 apart, ends reaching end_overlap into walls.
Polylines multiline_paths(const Lines &lines, double d1, double end_overlap, int sweep, const BoundingBox &cover);
//
// Some of the algorithms used by class FillAdaptive were inspired by
// Cura Engine's class SubDivCube
+174 -104
View File
@@ -3047,12 +3047,55 @@ bool FillRectilinear::fill_surface_by_multilines(const Surface *surface, FillPar
return true;
}
// Upper level of a cubic band [0, h] over one period, from the crossing at (0, tau) to the one at (period, tau).
// See docs/HLSD/multiline-infill.md.
static std::vector<Vec2d> cubic_upper_level(double tau, double h, double period, double d1)
{
const double s3 = std::sqrt(3.);
const double y_cut = std::clamp(tau - 0.5 * d1, 0., h - d1) + d1;
const double y_flat = std::min(h, h + y_cut - 2. * d1);
const double x2 = (h - tau) / s3 + d1;
const double x3 = (h + tau) / s3 - d1;
// (slope, intercept) of the rising line, its chamfer, the horizontal line, the falling chamfer and line.
const std::array<Vec2d, 5> lines{ Vec2d(s3, tau), Vec2d(1. / s3, h - x2 / s3), Vec2d(0., y_flat),
Vec2d(-1. / s3, h + x3 / s3), Vec2d(-s3, tau + s3 * period) };
auto y_at = [&lines, y_cut](double x) {
double y = std::numeric_limits<double>::max();
for (const Vec2d &l : lines)
y = std::min(y, l.x() * x + l.y());
return std::max(y, y_cut);
};
std::vector<double> xs;
for (size_t i = 0; i < lines.size(); ++i) {
if (lines[i].x() != 0.)
xs.emplace_back((y_cut - lines[i].y()) / lines[i].x());
for (size_t j = i + 1; j < lines.size(); ++j)
xs.emplace_back((lines[j].y() - lines[i].y()) / (lines[i].x() - lines[j].x()));
}
xs.erase(std::remove_if(xs.begin(), xs.end(), [period](double x) { return x <= 1. || x >= period - 1.; }), xs.end());
xs.insert(xs.end(), { 0., period });
std::sort(xs.begin(), xs.end());
xs.erase(std::unique(xs.begin(), xs.end(), [](double a, double b) { return b - a < 1.; }), xs.end());
std::vector<Vec2d> pts;
for (double x : xs) {
const Vec2d p(x, y_at(x));
if (pts.size() >= 2) {
const Vec2d &a = pts[pts.size() - 2], &b = pts.back();
if (std::abs((b.y() - a.y()) / (b.x() - a.x()) - (p.y() - b.y()) / (p.x() - b.x())) < EPSILON)
pts.pop_back();
}
pts.emplace_back(p);
}
return pts;
}
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
int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic
{
assert(params.multiline > 1);
@@ -3082,9 +3125,20 @@ 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
@@ -3104,21 +3158,31 @@ 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();
const coord_t ymax = bb.max.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;
// Create the two base row patterns once
Polyline base_row_normal;
base_row_normal.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid
base_row_normal.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid
Polyline base_row_flipped;
base_row_flipped.points.reserve(((xmax - xmin) / period + 1) * 5); // 5 points per trapezoid
base_row_flipped.points.reserve(((last_x - first_x) / period + 1) * 5); // 5 points per trapezoid
// Build complete rows from xmin to xmax
for (coord_t x = xmin; x < xmax; x += period) {
// Build rows on the same global period grid, limited to the surface cover.
for (coord_t x = first_x; x < last_x; 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
@@ -3134,13 +3198,14 @@ bool FillRectilinear::fill_surface_trapezoidal(
}
// Pre-allocate polylines
const size_t estimated_rows = ((ymax - ymin) / (period / 2) + 1);
const size_t estimated_rows = size_t(last_row - first_row + 1);
polylines.reserve(estimated_rows);
bool flip_vertical = false;
bool flip_vertical = (first_row % 2) != 0;
// Now just copy and translate vertically
for (coord_t y = ymin; y < ymax; y += period / 2) {
// 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;
Polyline pl_row = flip_vertical ? base_row_flipped : base_row_normal;
// Translate all points vertically
@@ -3156,16 +3221,10 @@ 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) {
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;
}
}
}
if (infill_layer_id % 2 == 1)
for (Polyline& pl : polylines)
for (Point& p : pl.points)
p = transpose(p);
break;
}
@@ -3186,40 +3245,24 @@ 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
// 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;
// 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;
// Pre-allocate estimated number of polylines
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
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
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 + 1) * 5); // 5 points per trapezoid
trapezoid_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period) * 5); // 5 points per trapezoid
Polyline trapezoid_row_shifted;
trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 5); // 5 points per trapezoid
trapezoid_row_shifted.points.reserve(((x_max_aligned - x_min_aligned) / period) * 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));
@@ -3239,10 +3282,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
for (coord_t y = y_min_aligned; y < y_max_aligned; y += h) {
bool shift_row = (first_row % 2) != 0;
for (coord_t row = first_row; row <= last_row; ++row) {
const coord_t y = row * h;
// Base line - copy and translate
Polyline base_line = base_line_template;
for (Point& p : base_line.points) {
@@ -3262,12 +3305,6 @@ 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;
}
@@ -3281,36 +3318,25 @@ 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;
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;
// 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;
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;
const size_t estimated_rows = size_t(last_row - first_row + 1);
const size_t estimated_polylines = estimated_rows * 2;
polylines.reserve(estimated_polylines);
Polyline star_row_normal;
star_row_normal.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7);
star_row_normal.points.reserve(size_t(last_tile - first_tile) * 7);
Polyline star_row_mirrored;
star_row_mirrored.points.reserve(((x_max_aligned - x_min_aligned) / period + 1) * 7);
star_row_mirrored.points.reserve(size_t(last_tile - first_tile) * 7);
for (coord_t x = x_min_aligned; x < x_max_aligned; x += period) {
for (int64_t tile = first_tile; tile < last_tile; ++tile) {
const coord_t x = coord_t(tile * period) - half_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
@@ -3324,9 +3350,7 @@ 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) {
@@ -3337,16 +3361,46 @@ bool FillRectilinear::fill_surface_trapezoidal(
polylines.emplace_back(std::move(row));
};
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);
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);
}
break;
}
if (layer_mod)
for (auto& pl : polylines)
pl.rotate(angle, Point(0, 0));
case 3: // Cubic
{
// Same z shifted lines as the single-line cubic; the slanted ones cross tau above the horizontal ones.
auto pos_mod = [](double a, double m) { const double r = std::fmod(a, m); return r < 0. ? r + m : r; };
const double h = 0.5 * std::sqrt(3.0) * period;
const double shift = scale_(std::sqrt(0.5) * this->z);
const double tau = pos_mod(-3. * shift, h);
const double y0 = pos_mod(-2. * shift, 2. * h);
std::array<std::vector<Vec2d>, 2> levels{ cubic_upper_level(h - tau, h, period, d1), cubic_upper_level(tau, h, period, d1) };
for (Vec2d &p : levels.front())
p.y() = h - p.y();
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;
const int64_t j_min = int64_t(std::floor((cover.min.x() - x_off) / period)) - 1;
const int64_t j_max = int64_t(std::ceil((cover.max.x() - x_off) / period));
for (const std::vector<Vec2d> &level : levels) {
Polyline row;
row.points.reserve(size_t(j_max - j_min + 1) * level.size());
for (int64_t j = j_min; j <= j_max; ++j)
for (size_t i = (j == j_min) ? 0 : 1; i < level.size(); ++i)
row.points.emplace_back(coord_t(std::round(x_off + double(j * period) + level[i].x())),
coord_t(std::round(base + level[i].y())));
polylines.emplace_back(std::move(row));
}
}
break;
}
@@ -3355,21 +3409,38 @@ bool FillRectilinear::fill_surface_trapezoidal(
break;
}
// 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.
// 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.
if (Pattern_type != 0)
for (Polyline &pl : polylines)
for (Polyline &pl : polylines) {
if (layer_mod)
pl.rotate(angle, Point(0, 0));
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)));
@@ -3452,9 +3523,7 @@ Polylines FillGrid::fill_surface(const Surface *surface, const FillParams &param
if (params.multiline > 1) {
// Experimental trapezoidal grid
if (!this->fill_surface_trapezoidal(
surface, params,
{ { 0.f, 0.f }, { float(M_PI / 2.), 0.f } },
polylines_out,0))
surface, params, polylines_out, 0))
BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed.";
} else {
@@ -3494,9 +3563,7 @@ Polylines FillTriangles::fill_surface(const Surface *surface, const FillParams &
if (params.multiline > 1) {
// Experimental trapezoidal grid
if (!this->fill_surface_trapezoidal(
surface, params,
{ { 0.f, 0.f }, { float(M_PI / 2.), 0.f } },
polylines_out,1))
surface, params, polylines_out, 1))
BOOST_LOG_TRIVIAL(error) << "FillGrid::fill_surface_trapezoidal() failed.";
} else {
@@ -3515,9 +3582,7 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams &para
Polylines polylines_out;
if (params.multiline > 1) {
if (!this->fill_surface_trapezoidal(
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))
surface, params, polylines_out, 2))
BOOST_LOG_TRIVIAL(error) << "FillStars::fill_surface_trapezoidal() failed.";
} else {
if (! this->fill_surface_by_multilines(
@@ -3532,6 +3597,11 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams &para
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams &params)
{
Polylines polylines_out;
if (params.multiline > 1) {
if (!this->fill_surface_trapezoidal(surface, params, polylines_out, 3))
BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed.";
return polylines_out;
}
coordf_t dx = sqrt(0.5) * z;
if (! this->fill_surface_by_multilines(
surface, params,
+1 -1
View File
@@ -29,7 +29,7 @@ protected:
float pattern_shift;
};
bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out);
bool fill_surface_trapezoidal(const Surface *surface, FillParams params, const std::initializer_list<SweepParams> &sweep_params, Polylines &polylines_out,int Pattern_type);
bool fill_surface_trapezoidal(const Surface *surface, FillParams 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;
+68 -12
View File
@@ -125,6 +125,10 @@ static constexpr const char* VOLUME_TYPE = "volume";
static constexpr const char* NAME_KEY = "name";
static constexpr const char* MODIFIER_KEY = "modifier";
static constexpr const char* VOLUME_TYPE_KEY = "volume_type";
// Keep seam modes separate from the base type so older readers see a non-printing modifier.
static constexpr const char* PRECISE_SEAM_TYPE_KEY = "precise_seam_type";
// Preserve dormant settings without turning an older reader's modifier into an active override.
static constexpr char PRECISE_SEAM_CONFIG_PREFIX[] = "precise_seam_config:";
static constexpr const char* MATRIX_KEY = "matrix";
static constexpr const char* SOURCE_FILE_KEY = "source_file";
static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id";
@@ -307,14 +311,17 @@ 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;
void *parser_buffer = XML_GetBuffer(m_parser, (int) stat.m_uncomp_size);
const int xml_size = static_cast<int>(stat.m_uncomp_size);
void *parser_buffer = XML_GetBuffer(m_parser, xml_size);
if (parser_buffer == nullptr) goto EXIT;
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, (size_t) stat.m_uncomp_size, 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
if (res == 0) goto EXIT;
XML_ParseBuffer(m_parser, (int) stat.m_uncomp_size, 1);
XML_ParseBuffer(m_parser, xml_size, 1);
}
}
@@ -368,7 +375,14 @@ ModelVolumeType type_from_string(const std::string &s)
if (s == "ParameterModifier") return ModelVolumeType::PARAMETER_MODIFIER;
if (s == "SupportEnforcer") return ModelVolumeType::SUPPORT_ENFORCER;
if (s == "SupportBlocker") return ModelVolumeType::SUPPORT_BLOCKER;
// Default value if invalud type string received.
// Precise Seam types (snake_case strings from ModelVolume::type_to_string)
if (s == "precise_seam_center") return ModelVolumeType::PRECISE_SEAM_CENTER;
if (s == "precise_seam_left") return ModelVolumeType::PRECISE_SEAM_LEFT;
if (s == "precise_seam_right") return ModelVolumeType::PRECISE_SEAM_RIGHT;
if (s == "precise_seam_enforced") return ModelVolumeType::PRECISE_SEAM_ENFORCED;
if (s == "precise_seam_blocked") return ModelVolumeType::PRECISE_SEAM_BLOCKED;
if (s == "precise_seam_neutral") return ModelVolumeType::PRECISE_SEAM_NEUTRAL;
// Default value if invalid type string received.
return ModelVolumeType::MODEL_PART;
}
@@ -1346,19 +1360,26 @@ 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);
void* parser_buffer = XML_GetBuffer(m_xml_parser, (int)stat.m_uncomp_size);
// 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);
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, (size_t)stat.m_uncomp_size, 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
if (res == 0) {
add_error("Error while reading config data to buffer");
return false;
}
if (!XML_ParseBuffer(m_xml_parser, (int)stat.m_uncomp_size, 1)) {
if (!XML_ParseBuffer(m_xml_parser, xml_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);
@@ -2035,6 +2056,7 @@ ModelVolumeType type_from_string(const std::string &s)
std::vector<std::string> valid_keys = {
"name",
"volume_type",
PRECISE_SEAM_TYPE_KEY,
"matrix",
"source_file",
"source_object_id",
@@ -2047,7 +2069,7 @@ ModelVolumeType type_from_string(const std::string &s)
};
auto itor = std::find(valid_keys.begin(), valid_keys.end(), key);
if (itor == valid_keys.end()) {
if (itor == valid_keys.end() && !(type == VOLUME_TYPE && boost::starts_with(key, PRECISE_SEAM_CONFIG_PREFIX))) {
// do nothing if not valid keys
return true;
}
@@ -2180,6 +2202,8 @@ ModelVolumeType type_from_string(const std::string &s)
volume->mmu_segmentation_facets.shrink_to_fit();
volume->fuzzy_skin_facets.shrink_to_fit();
// Apply the seam mode after all base-type metadata, regardless of XML key order.
ModelVolumeType precise_seam_type = ModelVolumeType::INVALID;
// apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY)
@@ -2188,6 +2212,10 @@ ModelVolumeType type_from_string(const std::string &s)
volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
else if (metadata.key == VOLUME_TYPE_KEY)
volume->set_type(type_from_string(metadata.value));
else if (metadata.key == PRECISE_SEAM_TYPE_KEY)
precise_seam_type = ModelVolume::type_from_string(metadata.value);
else if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX))
continue; // Restore dormant settings only after the final volume type is known.
else if (metadata.key == SOURCE_FILE_KEY)
volume->source.input_file = metadata.value;
else if (metadata.key == SOURCE_OBJECT_ID_KEY)
@@ -2208,6 +2236,22 @@ ModelVolumeType type_from_string(const std::string &s)
volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions);
}
// Missing or unknown seam modes retain the ordinary modifier fallback.
// Ignore seam metadata on other base types; legacy inline seam types still load above.
if (volume->is_modifier() && is_precise_seam(precise_seam_type))
volume->set_type(precise_seam_type);
// Unknown seam modes must remain inert modifiers, even when dormant settings are present.
if (volume->is_precise_seam()) {
for (const Metadata& metadata : volume_data.metadata) {
if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX)) {
const std::string key = metadata.key.substr(sizeof(PRECISE_SEAM_CONFIG_PREFIX) - 1);
if (!key.empty())
volume->config.set_deserialize(key, metadata.value, config_substitutions);
}
}
}
// this may happen for 3mf saved by 3rd part softwares
if (volume->name.empty()) {
volume->name = object.name;
@@ -3123,11 +3167,18 @@ ModelVolumeType type_from_string(const std::string &s)
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n";
// stores volume's modifier field (legacy, to support old slicers)
if (volume->is_modifier())
// Readers with only the legacy flag still see helper geometry as a modifier.
if (volume->is_modifier() || volume->is_precise_seam())
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n";
// stores volume's type (overrides the modifier field above)
// This Prusa-format reader uses ParameterModifier, not Bambu's modifier_part.
// The base type overrides the legacy flag, so it must also be backward-compatible.
// Use the same spelling for ordinary modifiers, including a downgraded seam helper.
const bool store_as_modifier = volume->is_modifier() || volume->is_precise_seam();
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " <<
VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n";
VALUE_ATTR << "=\"" << (store_as_modifier ? "ParameterModifier" : ModelVolume::type_to_string(volume->type())) << "\"/>\n";
if (volume->is_precise_seam())
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << PRECISE_SEAM_TYPE_KEY << "\" " <<
VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n";
// stores volume's local matrix
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\"";
@@ -3162,7 +3213,12 @@ ModelVolumeType type_from_string(const std::string &s)
// stores volume's config data
for (const std::string& key : volume->config.keys()) {
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n";
// Seam settings are inactive but must survive changing the helper back into a part/modifier.
const bool dormant = volume->is_precise_seam();
const std::string stored_key = dormant ? PRECISE_SEAM_CONFIG_PREFIX + key : key;
const std::string value = volume->config.opt_serialize(key);
// Config serialization is C-style, not XML: escape active settings too, including tabs.
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << stored_key << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n";
}
// stores mesh's statistics
+21 -8
View File
@@ -48,24 +48,37 @@ bool load_drc(const char *path, TriangleMesh *meshptr)
indexed_triangle_set its;
const PointAttribute *const positions = dracoMesh.GetNamedAttribute(GeometryAttribute::POSITION);
if (positions == nullptr) {
BOOST_LOG_TRIVIAL(error) << "load_drc: the mesh has no POSITION attribute";
return false;
}
size_t num_vertices = positions->size();
its.vertices.reserve(num_vertices);
for (AttributeValueIndex i(0); i < num_vertices; ++ i) {
float pos[3];
positions->ConvertValue<float>(i, 3, pos);
if (!positions->ConvertValue<float>(i, 3, pos)) {
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex position";
return false;
}
its.vertices.emplace_back(pos[0], pos[1], pos[2]);
}
// The Draco decoder does not check face indices against the point count.
const uint32_t num_points = dracoMesh.num_points();
size_t num_faces = dracoMesh.num_faces();
its.indices.reserve(num_faces);
for (FaceIndex i(0); i < num_faces; ++ i) {
Mesh::Face face = dracoMesh.face(i);
its.indices.emplace_back(
positions->mapped_index(face[0]).value(),
positions->mapped_index(face[1]).value(),
positions->mapped_index(face[2]).value()
);
const Mesh::Face &face = dracoMesh.face(i);
stl_triangle_vertex_indices facet;
for (int k = 0; k < 3; ++ k) {
const size_t vertex_idx = face[k].value() < num_points ? positions->mapped_index(face[k]).value() : num_vertices;
if (vertex_idx >= num_vertices) {
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex index";
return false;
}
facet[k] = static_cast<int>(vertex_idx);
}
its.indices.emplace_back(facet);
}
*meshptr = TriangleMesh(std::move(its));
+9 -4
View File
@@ -166,10 +166,15 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
obj_info.uv_map_pngs[face_index] = png_name;
}
if (data.textureCoordinates.size() > 0) {
Vec2f uv0(data.textureCoordinates[uvs[0] * 2], data.textureCoordinates[uvs[0] * 2 + 1]);
Vec2f uv1(data.textureCoordinates[uvs[1] * 2], data.textureCoordinates[uvs[1] * 2 + 1]);
Vec2f uv2(data.textureCoordinates[uvs[2] * 2], data.textureCoordinates[uvs[2] * 2 + 1]);
std::array<Vec2f, 3> uv_array{uv0, uv1, uv2};
// A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than
// skipping the face, so obj_info.uvs stays aligned with the face indices.
const int uv_count = static_cast<int>(data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH);
auto uv_at = [&data, uv_count](int idx) -> Vec2f {
if (idx < 0 || idx >= uv_count)
return Vec2f::Zero();
return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
};
std::array<Vec2f, 3> uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])};
obj_info.uvs.emplace_back(uv_array);
}
obj_info.face_colors.emplace_back(face_color);
+1
View File
@@ -25,6 +25,7 @@
#include "XCAFDoc_DocumentTool.hxx"
#include "XCAFDoc_ShapeTool.hxx"
#include "XCAFApp_Application.hxx"
#include "TDF_LabelSequence.hxx"
#include "TopoDS_Solid.hxx"
#include "TopoDS_Compound.hxx"
#include "TopoDS_Builder.hxx"
+50 -7
View File
@@ -353,6 +353,10 @@ static constexpr const char* PART_TYPE = "part";
static constexpr const char* NAME_KEY = "name";
static constexpr const char* VOLUME_TYPE_KEY = "volume_type";
static constexpr const char* PART_TYPE_KEY = "part_type";
// Keep seam modes separate from the base type so older readers see a non-printing modifier.
static constexpr const char* PRECISE_SEAM_TYPE_KEY = "precise_seam_type";
// Preserve dormant settings without turning an older reader's modifier into an active override.
static constexpr char PRECISE_SEAM_CONFIG_PREFIX[] = "precise_seam_config:";
static constexpr const char* MATRIX_KEY = "matrix";
static constexpr const char* SOURCE_FILE_KEY = "source_file";
static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id";
@@ -1629,7 +1633,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
while (it != m_plater_data.end())
{
if (it->first > m_plater_data.size())
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
{
add_error("invalid plate index");
return false;
@@ -2312,7 +2316,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
while (it != m_plater_data.end())
{
if (it->first > m_plater_data.size())
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
{
add_error("invalid plate index");
return false;
@@ -2508,19 +2512,26 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
XML_SetEntityDeclHandler(m_xml_parser, nullptr);
XML_SetExternalEntityRefHandler(m_xml_parser, nullptr);
void* parser_buffer = XML_GetBuffer(m_xml_parser, (int)stat.m_uncomp_size);
// 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);
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, (size_t)stat.m_uncomp_size, 0);
mz_bool res = mz_zip_reader_extract_file_to_mem(&archive, stat.m_filename, parser_buffer, static_cast<size_t>(xml_size), 0);
if (res == 0) {
add_error("Error while reading config data to buffer");
return false;
}
if (!XML_ParseBuffer(m_xml_parser, (int)stat.m_uncomp_size, 1)) {
if (!XML_ParseBuffer(m_xml_parser, xml_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);
@@ -5212,6 +5223,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (auto &tc = volume_data->text_configuration; tc.has_value())
volume->text_configuration = std::move(tc);
// Apply the seam mode after all base-type metadata, regardless of XML key order.
ModelVolumeType precise_seam_type = ModelVolumeType::INVALID;
// apply the remaining volume's metadata
for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY)
@@ -5221,6 +5234,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//for old format
else if ((metadata.key == VOLUME_TYPE_KEY) || (metadata.key == PART_TYPE_KEY))
volume->set_type(ModelVolume::type_from_string(metadata.value));
else if (metadata.key == PRECISE_SEAM_TYPE_KEY)
precise_seam_type = ModelVolume::type_from_string(metadata.value);
else if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX))
continue; // Restore dormant settings only after the final volume type is known.
else if (metadata.key == SOURCE_FILE_KEY)
volume->source.input_file = metadata.value;
else if (metadata.key == SOURCE_OBJECT_ID_KEY)
@@ -5243,6 +5260,22 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions);
}
// Missing or unknown seam modes retain the ordinary modifier fallback.
// Ignore seam metadata on other base types; legacy inline seam types still load above.
if (volume->is_modifier() && is_precise_seam(precise_seam_type))
volume->set_type(precise_seam_type);
// Unknown seam modes must remain inert modifiers, even when dormant settings are present.
if (volume->is_precise_seam()) {
for (const Metadata& metadata : volume_data->metadata) {
if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX)) {
const std::string key = metadata.key.substr(sizeof(PRECISE_SEAM_CONFIG_PREFIX) - 1);
if (!key.empty())
volume->config.set_deserialize(key, metadata.value, config_substitutions);
}
}
}
// this may happen for 3mf saved by 3rd part softwares
if (volume->name.empty()) {
volume->name = object.name;
@@ -8002,7 +8035,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume_id = m_volume_paths.find(volume)->second.second;
stream << ID_ATTR << "=\"" << volume_id << "\" ";
stream << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n";
// Older slicers must recognize the base type even when they ignore seam metadata.
const ModelVolumeType stored_type = volume->is_precise_seam() ? ModelVolumeType::PARAMETER_MODIFIER : volume->type();
stream << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(stored_type) << "\">\n";
if (volume->is_precise_seam())
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << PRECISE_SEAM_TYPE_KEY << "\" " << VALUE_ATTR << "=\"" <<
ModelVolume::type_to_string(volume->type()) << "\"/>\n";
//stream << " <" << PART_TAG << " " << ID_ATTR << "=\"" << it->second << "\" " << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n";
// stores volume's name
@@ -8052,7 +8090,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// stores volume's config data
for (const std::string& key : volume->config.keys()) {
stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n";
// Seam settings are inactive but must survive changing the helper back into a part/modifier.
const bool dormant = volume->is_precise_seam();
const std::string stored_key = dormant ? PRECISE_SEAM_CONFIG_PREFIX + key : key;
const std::string value = volume->config.opt_serialize(key);
// Config serialization is C-style, not XML: escape active settings too, including tabs.
stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << stored_key << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n";
}
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
+4 -5
View File
@@ -51,19 +51,18 @@ static bool obj_parseline(const char *line, ObjData &data)
line = endptr;
EATWS();
}
/*double w = 0;
// The optional w is accepted but not stored: only u and v are used.
if (*line != 0) {
w = strtod(line, &endptr);
strtod(line, &endptr);
if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0))
return false;
line = endptr;
EATWS();
}*/
}
if (*line != 0)
return false;
data.textureCoordinates.push_back((float)u);
data.textureCoordinates.push_back((float)v);
//data.textureCoordinates.push_back((float)w);
break;
}
case 'n':
@@ -245,7 +244,7 @@ static bool obj_parseline(const char *line, ObjData &data)
else
-- vertex.normalIdx;
if (vertex.textureCoordIdx < 0)
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 3;
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH;
else
-- vertex.textureCoordIdx;
data.vertices.push_back(vertex);
+2 -1
View File
@@ -92,6 +92,7 @@ inline bool operator==(const ObjSmoothingGroup &v1, const ObjSmoothingGroup &v2)
}
#define OBJ_VERTEX_COLOR_ALPHA 6
#define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w
#define OBJ_TEXCOORD_LENGTH 2 // u, v
#define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1
struct ObjData {
// Version of the data structure for load / store in the private binary format.
@@ -100,7 +101,7 @@ struct ObjData {
// x, y, z, color_x,color_y,color_z,color_w
std::vector<float> coordinates;
bool has_vertex_color{false};
// u, v, w
// u, v
std::vector<float> textureCoordinates;
// x, y, z
std::vector<float> normals;
+265 -198
View File
@@ -8,6 +8,7 @@
#include "I18N.hpp"
#include "GCode.hpp"
#include "Exception.hpp"
#include "LifecycleEvents.hpp"
#include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp"
#include "Geometry/ConvexHull.hpp"
@@ -969,6 +970,27 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return gcode;
}
// A folded tower layer is thicker than the object layer it sits on, so the height process_layer
// emitted is not the tower's. Both writers declare one, but each hardcodes a tag dialect - Type 1
// forces s_IsBBLPrinter and writes "; LAYER_HEIGHT:", Type 2 writes ";HEIGHT:" - and the processor
// reads only its printer's, so a Type 1 tower on a non-BBL printer loses it and the merged layer
// is drawn and costed as a thin one. Declare it here, where the printer is known, unless the tower
// already wrote the right tag. _extrude puts the object's height back on the next object path,
// since process_layer forces the role to erWipeTower on any layer with a tower.
std::string WipeTowerIntegration::tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr,
const std::string &tcr_gcode) const
{
const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height);
if (! m_sparse_layers_combined || std::abs(gcodegen.m_last_height - tcr.layer_height) <= EPSILON ||
tcr_gcode.find(tag) != std::string::npos)
return {};
// Keep m_last_height what the G-code last declared, so a second visit does not repeat it.
gcodegen.m_last_height = tcr.layer_height;
char buf[64];
sprintf(buf, "%s%g\n", tag.c_str(), tcr.layer_height);
return buf;
}
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
{
if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
@@ -1466,6 +1488,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
config.set_key_value("filament_start_gcode", new ConfigOptionString(start_filament_gcode_str));
std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_filament_id, &config);
unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode);
gcode += tower_height_tag(gcodegen, tcr, tcr_gcode);
gcode += tcr_gcode;
// Count the toolchange only when the emitted block really changed the tool —
// tower visits without a filament change must not advance the ordinal.
@@ -1798,6 +1821,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string tcr_gcode,
tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config);
unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode);
gcode += tower_height_tag(gcodegen, tcr, tcr_gcode);
gcode += tcr_gcode;
check_add_eol(toolchange_gcode_str);
@@ -1945,7 +1969,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// Calculate where the wipe tower layer will be printed. -1 means that print z will not change,
// resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1;
bool ignore_sparse = false;
// Folded into a later, thicker layer that prints at its own z: nothing to emit.
bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]);
if (m_sparse_layers_skipped) {
wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
@@ -1963,7 +1988,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// Calculate where the wipe tower layer will be printed. -1 means that print z will not change,
// resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1;
bool ignore_sparse = false;
// Folded into a later, thicker layer that prints at its own z: nothing to emit.
bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]);
if (m_sparse_layers_skipped) {
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
wipe_tower_z = m_compacted_tower_z[m_layer_idx];
@@ -1993,7 +2019,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (m_layer_idx >= (int) m_tool_changes.size())
return true;
bool ignore_sparse = false;
bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]);
if (m_sparse_layers_skipped)
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
@@ -2476,6 +2502,16 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_writer.set_is_bbl_machine(print->is_BBL_printer());
print->set_started(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
// check if any custom gcode contains keywords used by the gcode processor to
// produce time estimation and gcode toolpaths
std::vector<std::pair<std::string, std::string>> validation_res = DoExport::validate_custom_gcode(*print);
@@ -2511,12 +2547,24 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_processor.set_print(print);
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
if (! file.is_open()) {
BOOST_LOG_TRIVIAL(error) << std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n" << std::endl;
std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
if (!fs::exists(folder)) {
//fs::create_directory(folder);
BOOST_LOG_TRIVIAL(error) << "the parent path " + folder.string() +" is not there!!!" << std::endl;
std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
err_msg += add_err_msg;
}
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n");
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(err_msg);
}
try {
@@ -2527,11 +2575,20 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str());
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
}
} catch (std::exception & /* ex */) {
} catch (std::exception &ex) {
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
// Close and remove the file.
file.close();
boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw;
}
file.close();
@@ -2637,6 +2694,15 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
std::error_code ret = rename_file(path_tmp, path);
if (ret) {
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
"Is " + path_tmp + " locked?" + '\n');
@@ -2647,7 +2713,17 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info();
print->set_done(psGCodeExport);
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
// Orca: label_object_enabled reflects whether objects are labeled in the g-code (EXCLUDE_OBJECT /
// M486), which is driven by exclude_object for every printer
if(result != nullptr)
@@ -3024,162 +3100,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
if (m_config.small_area_infill_flow_compensation.value && !m_config.small_area_infill_flow_compensation_model.empty())
m_small_area_infill_flow_compensator = make_unique<SmallAreaInfillFlowCompensator>(print.config());
// Process file_start_gcode - written at the very top of the file, before any header
{
std::string top_gcode_template = print.config().file_start_gcode.value;
if (!top_gcode_template.empty()) {
DynamicConfig top_config;
// file_start_gcode runs before the parser copy that normally restores these, so set them here.
PlaceholderParser::update_timestamp(top_config);
PlaceholderParser::update_user_name(top_config);
top_config.set_key_value("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
top_config.set_key_value("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
top_config.set_key_value("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
top_config.set_key_value("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
top_config.set_key_value("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
top_config.set_key_value("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
std::string top_gcode = print.placeholder_parser().process(top_gcode_template, 0, &top_config);
if (!top_gcode.empty())
file.writeln(top_gcode);
}
}
// Orca: Don't output Header block if BTT thumbnail is identified in the list
// Get the thumbnails value as a string
std::string thumbnails_value = print.config().option<ConfigOptionString>("thumbnails")->value;
// search string for the BTT_TFT label
bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos);
if(!has_BTT_thumbnail){
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str());
//Orca: extra check for bbl printer
if (is_bbl_printers) {
if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode
// list all label_object_id with sorted order here
m_enable_exclude_object = true;
m_label_objects_ids.clear();
m_label_objects_ids.reserve(print.num_object_instances());
for (const PrintObject *print_object : print.objects())
for (const PrintInstance &print_instance : print_object->instances())
m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id());
std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end());
std::string objects_id_list = "; model label id: ";
for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++)
objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n"));
file.writeln(objects_id_list);
} else {
m_enable_exclude_object = false;
m_label_objects_ids.clear();
}
}
{
std::string filament_density_list = "; filament_density: ";
(filament_density_list+=m_config.filament_density.serialize()) +='\n';
file.writeln(filament_density_list);
std::string filament_diameter_list = "; filament_diameter: ";
(filament_diameter_list += m_config.filament_diameter.serialize()) += '\n';
file.writeln(filament_diameter_list);
coordf_t max_height_z = -1;
for (const auto& object : print.objects())
max_height_z = std::max(object->layers().back()->print_z, max_height_z);
std::ostringstream max_height_z_tip;
max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n';
file.writeln(max_height_z_tip.str());
}
{
auto used_filaments = print.get_slice_used_filaments(false);
std::ostringstream out;
out << "; filament: ";
for (size_t idx = 0; idx < used_filaments.size(); ++idx) {
if (idx != 0)
out << ',';
out << used_filaments[idx] + 1;
}
file.writeln(out.str());
}
file.write_format("; HEADER_BLOCK_END\n\n");
}
// BBS: write global config at the beginning of gcode file because printer
// need these config information
// Append full config, delimited by two 'phony' configuration keys
// CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
if (!thumbnails.empty())
GCodeThumbnails::export_thumbnails_to_file(
thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); });
}
}
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
const double layer_height = first_object->config().layer_height.value;
const double initial_layer_print_height = print.config().initial_layer_print_height.value;
for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) {
const PrintRegion &region = print.get_print_region(region_id);
file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width());
file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width());
file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width());
file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width());
file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width());
if (print.has_support_material())
file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width());
if (print.config().initial_layer_line_width.value > 0)
file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width());
file.write_format("\n");
}
file.write_format("; EXECUTABLE_BLOCK_START\n");
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str());
// Prepare the helper object for replacing placeholders in custom G-code and output filename.
m_placeholder_parser_integration.parser = print.placeholder_parser();
m_placeholder_parser_integration.parser.update_timestamp();
@@ -3314,16 +3234,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Orca: Initialise AdaptivePA processor filter
m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this, tool_ordering.all_extruders());
// Emit machine envelope limits for the Marlin firmware.
this->print_machine_envelope(file, print);
// Disable fan.
if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) {
file.write(m_writer.set_fan(0));
//BBS: disable additional fan
file.write(m_writer.set_additional_fan(0));
}
// Update output variables after the extruders were initialized.
m_placeholder_parser_integration.init(m_writer);
@@ -3675,6 +3585,157 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params();
// Expand the file header only after the start-up placeholders and writer state are ready.
// Use the regular custom G-code path so invalid placeholders report the template name.
if (!print.config().file_start_gcode.value.empty())
file.writeln(this->placeholder_parser_process("file_start_gcode", print.config().file_start_gcode.value, initial_extruder_id));
// Orca: Don't output Header block if BTT thumbnail is identified in the list
// Get the thumbnails value as a string
std::string thumbnails_value = print.config().option<ConfigOptionString>("thumbnails")->value;
// search string for the BTT_TFT label
bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos);
if(!has_BTT_thumbnail){
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str());
//Orca: extra check for bbl printer
if (is_bbl_printers) {
if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode
// list all label_object_id with sorted order here
m_enable_exclude_object = true;
m_label_objects_ids.clear();
m_label_objects_ids.reserve(print.num_object_instances());
for (const PrintObject *print_object : print.objects())
for (const PrintInstance &print_instance : print_object->instances())
m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id());
std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end());
std::string objects_id_list = "; model label id: ";
for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++)
objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n"));
file.writeln(objects_id_list);
} else {
m_enable_exclude_object = false;
m_label_objects_ids.clear();
}
}
{
std::string filament_density_list = "; filament_density: ";
(filament_density_list+=m_config.filament_density.serialize()) +='\n';
file.writeln(filament_density_list);
std::string filament_diameter_list = "; filament_diameter: ";
(filament_diameter_list += m_config.filament_diameter.serialize()) += '\n';
file.writeln(filament_diameter_list);
coordf_t max_height_z = -1;
for (const auto& object : print.objects())
max_height_z = std::max(object->layers().back()->print_z, max_height_z);
std::ostringstream max_height_z_tip;
max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n';
file.writeln(max_height_z_tip.str());
}
{
auto used_filaments = print.get_slice_used_filaments(false);
std::ostringstream out;
out << "; filament: ";
for (size_t idx = 0; idx < used_filaments.size(); ++idx) {
if (idx != 0)
out << ',';
out << used_filaments[idx] + 1;
}
file.writeln(out.str());
}
file.write_format("; HEADER_BLOCK_END\n\n");
}
// BBS: write global config at the beginning of gcode file because printer
// need these config information
// Append full config, delimited by two 'phony' configuration keys
// CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
if (!thumbnails.empty())
GCodeThumbnails::export_thumbnails_to_file(
thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); });
}
}
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
const double layer_height = first_object->config().layer_height.value;
const double initial_layer_print_height = print.config().initial_layer_print_height.value;
for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) {
const PrintRegion &region = print.get_print_region(region_id);
file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width());
file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width());
file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width());
file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width());
file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width());
if (print.has_support_material())
file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width());
if (print.config().initial_layer_line_width.value > 0)
file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width());
file.write_format("\n");
}
file.write_format("; EXECUTABLE_BLOCK_START\n");
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str());
// Emit machine envelope limits for the Marlin firmware.
this->print_machine_envelope(file, print);
// Disable fan.
if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) {
file.write(m_writer.set_fan(0));
//BBS: disable additional fan
file.write(m_writer.set_additional_fan(0));
}
std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id);
if (print.config().gcode_flavor != gcfKlipper) {
// Set bed temperature if the start G-code does not contain any bed temp control G-codes.
@@ -4137,6 +4198,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Const input (tool-change fallback for non-wipe-tower prints)
print.tool_ordering()));
print.m_print_statistics.initial_tool = initial_extruder_id;
print.m_print_statistics.initial_no_support_tool = initial_non_support_extruder_id;
if (!is_bbl_printers) {
file.write_format("; total filament used [g] = %.2lf\n",
print.m_print_statistics.total_weight);
@@ -7908,6 +7970,20 @@ double GCode::calc_max_volumetric_speed(const double layer_height, const double
return res;
}
// ORCA: Overlap at or below which the overhang fan switches on; negative when it does not depend on overlap
// (Overhang_threshold_none cools every external perimeter).
static float overhang_fan_overlap_threshold(int overhang_fan_threshold)
{
switch (overhang_fan_threshold) {
case (int) Overhang_threshold_1_4: return 0.9f;
case (int) Overhang_threshold_2_4: return 0.75f;
case (int) Overhang_threshold_3_4: return 0.5f;
case (int) Overhang_threshold_4_4: return 0.25f;
case (int) Overhang_threshold_bridge: return 0.05f;
default: return -1.f;
}
}
std::string GCode::_extrude(const ExtrusionPath &path, std::string description, double speed)
{
std::string gcode;
@@ -8038,7 +8114,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
_mm3_per_mm *= filament_flow_ratio;
if (path.role() == erTopSolidInfill) {
_mm3_per_mm *= m_config.top_solid_infill_flow_ratio;
_mm3_per_mm *= NOZZLE_CONFIG(top_solid_infill_flow_ratio);
} else if (path.role() == erBottomSurface) {
_mm3_per_mm *= m_config.bottom_solid_infill_flow_ratio;
} else if (path.role() == erInternalBridgeInfill) {
@@ -8250,6 +8326,13 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
ConfigOptionPercents overhang_overlap_levels({90, 75, 50, 25, 13, 0});
// ORCA: Lets the path be split where the overhang fan switches, not only where the speed changes.
// Bridges and overhang perimeters are cooled regardless of overlap.
float fan_overlap_threshold = -1.f;
if (FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers && path.role() != erBridgeInfill &&
path.role() != erOverhangPerimeter)
fan_overlap_threshold = overhang_fan_overlap_threshold(FILAMENT_CONFIG(overhang_fan_threshold));
if (NOZZLE_CONFIG(slowdown_for_curled_perimeters)){
ConfigOptionFloatsOrPercents dynamic_overhang_speeds(
{FloatOrPercent{100, true},
@@ -8270,7 +8353,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
FloatOrPercent{NOZZLE_CONFIG(overhang_4_4_speed).get_abs_value(ref_speed) * 100 / ref_speed, true}});
new_points = m_extrusion_quality_estimator.estimate_extrusion_quality(path, overhang_overlap_levels, dynamic_overhang_speeds,
ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters));
ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters),
fan_overlap_threshold);
}else{
ConfigOptionFloatsOrPercents dynamic_overhang_speeds(
{FloatOrPercent{100, true},
@@ -8289,7 +8373,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
FloatOrPercent{NOZZLE_CONFIG(bridge_speed) * 100 / ref_speed, true}});
new_points = m_extrusion_quality_estimator.estimate_extrusion_quality(path, overhang_overlap_levels, dynamic_overhang_speeds,
ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters));
ref_speed, speed, NOZZLE_CONFIG(slowdown_for_curled_perimeters),
fan_overlap_threshold);
}
variable_speed = std::any_of(new_points.begin(), new_points.end(),
[speed](const ProcessedPoint &p) { return fabs(double(p.speed) - speed) > 1; }); // Ignore small speed variations (under 1mm/sec)
@@ -8441,28 +8526,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
if (role == erBridgeInfill || role == erOverhangPerimeter) { // ORCA: Split out bridge infill to internal and external to apply separate fan settings
return true;
}
switch (overhang_fan_threshold) {
case (int)Overhang_threshold_1_4:
return overlap <= 0.9f;
break;
case (int)Overhang_threshold_2_4:
return overlap <= 0.75f;
break;
case (int)Overhang_threshold_3_4:
return overlap <= 0.5f;
break;
case (int)Overhang_threshold_4_4:
return overlap <= 0.25f;
break;
case (int)Overhang_threshold_bridge:
return overlap <= 0.05f;
break;
case (int)Overhang_threshold_none:
if (overhang_fan_threshold == Overhang_threshold_none)
return is_external_perimeter(role);
break;
default:
return false;
}
const float overlap_threshold = overhang_fan_overlap_threshold(overhang_fan_threshold);
return overlap_threshold >= 0.f && overlap <= overlap_threshold;
};
std::string comment;
+6 -1
View File
@@ -107,7 +107,8 @@ public:
m_is_first_print(true),
m_print_config(&print_config),
m_last_wipe_tower_print_z(print_config.z_offset.value),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config))
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)),
m_sparse_layers_combined(wipe_tower_sparse_layers_combined(print_config))
{
// Precomputed rather than accumulated while emitting, so that the clearance validator and
// the emitter cannot disagree about where the compacted tower sits on any given layer.
@@ -138,6 +139,7 @@ public:
private:
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, const std::string &tcr_gcode) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const;
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const;
@@ -175,6 +177,9 @@ private:
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
const bool m_sparse_layers_skipped;
// Combined tower layers are thicker than the object layer they sit on, the only case where the
// tower's height is not the one process_layer already declared.
const bool m_sparse_layers_combined;
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
std::vector<float> m_compacted_tower_z;
};
+110 -74
View File
@@ -41,10 +41,12 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
float flow_width,
float max_line_length = -1.0f,
float min_distance = -1.0f,
// Maps an overhang distance onto the speed it will be printed at. Interior sampling
// needs it to tell which of the points it could add would change the G-code, and is
// skipped without it.
const std::function<float(float)>& distance_to_speed = {})
// Speed an overhang distance prints at. Without it, interior sampling
// is skipped and every line over 4mm is split.
const std::function<float(float)>& distance_to_speed = {},
// Overlap (1 - distance / flow_width) at or below which the overhang
// fan switches on; negative when the fan does not depend on overlap.
float fan_overlap_threshold = -1.0f)
{
bool looped = input_points.front() == input_points.back();
std::function<size_t(size_t,size_t)> get_prev_index = [](size_t idx, size_t count) {
@@ -125,29 +127,43 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
points.push_back(next_point);
}
// ORCA: How an overhang distance prints, which is what the passes below compare. A segment is printed at the lower
// of the speeds at its two ends, and with the overhang fan on if the overlap at either end turns it on. A point
// added to a path can therefore only change the G-code where it prints at a different speed or fan state from the
// points either side of it, and the passes below add points there and nowhere else.
const float width_inv = 1.f / flow_width;
// Whether an overhang distance turns the overhang fan on. The overlap test check_overhang_fan applies in GCode.cpp.
auto fan_on = [fan_overlap_threshold, width_inv](float distance) {
return fan_overlap_threshold >= 0.f && 1.f - distance * width_inv <= fan_overlap_threshold;
};
// Whether two overhang distances are interchangeable ie have the same speed (beyond a 1mm/sec threshold that gcode.cpp filters out on)
// and the same fan state.
auto same_speed_and_fan = [&distance_to_speed, &fan_on](float a, float b) {
return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f && fan_on(a) == fan_on(b);
};
// Whether the first overhang distance prints slower than the second, beyond the 1mm/sec gcode.cpp tolerance, or turns the overhang
// fan on where the second does not. Against a supported point (overhang distance 0) it tells whether an end is
// affected by the overhang.
auto slower_or_cooled = [&distance_to_speed, &fan_on](float a, float b) {
return distance_to_speed(a) < distance_to_speed(b) - 1.f || (fan_on(a) && !fan_on(b));
};
// ORCA: Interior sampling
// The passes below infer the support under a span from its endpoints alone, so an interior that is supported
// differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by
// full height walls reads as supported along its whole length. Probe the interior, keep the samples the
// endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly
// unsupported gets points where its support actually changes instead of one reading spread across all of it.
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) {
// The passes below infer the support under a span from its endpoints alone, so a part that is supported
// differently from both ends is invisible to them. The outer perimeter of an overhang whose ends are supported by
// reads as supported along its whole length. Probe the interior, keep the samples the endpoint interpolation fails to predict,
// and bisect either side of each one, so a span that is only partly unsupported gets points where its support actually changes
// instead of one reading being spread across all of it.
// Skipped where there is nothing to find: min_distance <= 0 when no overhang can slow this path down, and
// fan_overlap_threshold < 0 when no overhang switches the fan on. It also needs distance_to_speed to tell which of
// the points it could add would change the G-code.
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && distance_to_speed && (min_distance > 0 || fan_overlap_threshold >= 0.f)) {
// Probe at least this densely before treating matching samples as evidence that a span is uniform. The
// segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer
// together than min_spacing, so finer discovery would not produce a more precise speed transition.
// together than min_spacing, so finer discovery would not produce a more precise transition.
const double max_probe_spacing = std::max(2., 4. * min_spacing);
// A backstop for that length test, which on a non-finite length would never be met.
constexpr int max_bisection_depth = 10;
// Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends
// read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever
// its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections
// interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart.
// The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all.
auto same_speed = [&distance_to_speed](float a, float b) {
return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f;
};
// Whether the first reading is printed slower than the second, once they are known to differ.
auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); };
// Part of a segment still to bisect: its positions along the segment and bisections left.
struct Subspan { double t0, t1; int depth; };
@@ -185,8 +201,8 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
if (!interior.empty()) {
std::sort(interior.begin(), interior.end(),
[](const std::pair<double, float>& l, const std::pair<double, float>& r) { return l.first < r.first; });
// Coarse probing keeps every sample it took until this pass can see which ones bracket a speed
// transition. Matching samples cannot be discarded during discovery: one may be the last
// Coarse probing keeps every sample it took until this pass can see which ones bracket a speed or
// fan transition. Matching samples cannot be discarded during discovery: one may be the last
// supported point before a narrow unsupported pocket found by a later probe.
size_t kept = 0;
for (size_t i = 0; i < interior.size(); ++i) {
@@ -195,15 +211,15 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end
const float before = at_start ? curr.distance : interior[kept - 1].second;
const float after = at_end ? next.distance : interior[i + 1].second;
// A sample is worth a point in the path only where it prints at a different speed from the
// readings either side of it. Differing from one of the segment's own ends is not enough on
// its own where the sample is the faster of the two: the segmentation pass below already
// ends the slowdown an end reads, at a distance taken from how far out that end is rather
// than from wherever bisection happened to stop, and a point here would leave the span
// beside the end too short for that pass to run at all. Support an end cannot account for,
// where the interior is the slower reading, is exactly what this pass is here to find.
const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before));
const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after));
// A sample is worth a point in the path only where it prints differently, in speed or fan state,
// from the points either side of it. Differing from one of the segment's own ends is not enough on
// its own where the sample is not the slower or cooled of the two: the segmentation pass below
// already confines the slowdown and cooling at an end, at a distance taken from how far out that
// end is rather than from wherever bisection happened to stop, and a point here would leave the
// span beside the end too short for that pass to run at all. Support an end cannot account for,
// where the interior is the slower or cooled of the two, is exactly what this pass is here to find.
const bool worth_before = !same_speed_and_fan(sample, before) && (!at_start || slower_or_cooled(sample, before));
const bool worth_after = !same_speed_and_fan(sample, after) && (!at_end || slower_or_cooled(sample, after));
if (worth_before || worth_after)
interior[kept++] = interior[i];
}
@@ -238,52 +254,60 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
if ((curr.distance > -boundary_offset && curr.distance < boundary_offset + 2.0f) ||
(next.distance > -boundary_offset && next.distance < boundary_offset + 2.0f)) {
double line_len = (next.position - curr.position).norm();
// ORCA: Segment path to smaller lines by adding additional points only if the path has an overhang that
// will trigger a slowdown and the path is also reasonably large, i.e. 2mm in length or more
// If there is no overhang in the start/end point, dont segment it.
// Ignore this check if the control of segmentation for overhangs is disabled (min_distance=-1)
if ((min_distance > 0 && ((std::abs(curr.distance) > min_distance) || (std::abs(next.distance) > min_distance)) && line_len >= 2.f) ||
(min_distance <= 0 && line_len > 4.0f)) {
// ORCA: A line prints as slow as its slower end and is cooled if either end is, so an overhang at one
// end would otherwise slow down or cool the whole line. Split the line so that only the part beside
// that end prints that way, if the line is reasonably long (2mm or more) and at least one end prints
// slower or cooled compared with a supported point (overhang distance 0). Deciding on how the end
// prints, rather than on its overhang distance against min_distance, also catches an end whose overhang
// distance is exactly where the slowdown begins, such as an outline crossing at half a line width.
// Without distance_to_speed, split every line over 4mm.
const bool split_line = distance_to_speed ?
line_len >= 2.f && (slower_or_cooled(curr.distance, 0.f) || slower_or_cooled(next.distance, 0.f)) :
line_len > 4.0f;
if (split_line) {
// Each end's piece is that end's overhang distance plus 1.5 line widths (3 * boundary_offset) long:
// a0 ends the piece beside curr, a1 starts the piece beside next.
double a0 = std::clamp((curr.distance + 3 * boundary_offset) / line_len, 0.0, 1.0);
double a1 = std::clamp(1.0f - (next.distance + 3 * boundary_offset) / line_len, 0.0, 1.0);
double t0 = std::min(a0, a1);
double t1 = std::max(a0, a1);
if (t0 < 1.0) {
Vec p0 = curr.position + t0 * (next.position - curr.position);
auto [p0_dist, p0_near_l, p0_x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
p0.template cast<AABBScalar>());
ExtendedPoint<L::Dim> new_p{};
new_p.position = p0;
new_p.distance = float(p0_dist + boundary_offset);
// ORCA: only create a new point in the path if the new point overhang distance will be used to generate a speed change
// or if this option is disabled (min_distance<=0)
if( (std::abs(p0_dist) > min_distance) || (min_distance<=0)){
// ORCA: also filter out points that are introduced to the start of the path when their distance from the start point is
// not meaningful
if ((p0 - curr.position).norm() > min_spacing && (next.position - p0).norm() > min_spacing) {
new_points.push_back(new_p);
}
}
// Up to two cut points, in order along the line. Each takes its own overhang distance, so every
// piece prints by the overhang distances at its own two ends. t0 >= 1 or t1 <= 0 falls on the
// line's own end, so there is no cut. A cut closer than min_spacing to either end of the line is
// not meaningful and is filtered out (#6714).
ExtendedPoint<L::Dim> cut[2]{};
bool keep[2] = {false, false};
for (int k = 0; k < 2; ++k) {
const double t = k == 0 ? t0 : t1;
if (k == 0 ? t >= 1.0 : t <= 0.0)
continue;
const Vec p = curr.position + t * (next.position - curr.position);
auto [p_dist, p_near_l, p_x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
p.template cast<AABBScalar>());
cut[k].position = p;
cut[k].distance = float(p_dist + boundary_offset);
keep[k] = (p - curr.position).norm() > min_spacing && (next.position - p).norm() > min_spacing;
}
if (t1 > 0.0) {
Vec p1 = curr.position + t1 * (next.position - curr.position);
auto [p1_dist, p1_near_l, p1_x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
p1.template cast<AABBScalar>());
ExtendedPoint<L::Dim> new_p{};
new_p.position = p1;
new_p.distance = float(p1_dist + boundary_offset);
// ORCA: only create a new point in the path if the new point overhang distance will be used to generate a speed change
// or if this option is disabled (min_distance<=0)
if( (std::abs(p1_dist) > min_distance) || (min_distance<=0)){
// ORCA: filter out points that are introduced to the end of the path when their distance from the end point is
// not meaningful
if ((p1 - curr.position).norm() > min_spacing && (next.position - p1).norm() > min_spacing) {
new_points.push_back(new_p);
}
}
if (distance_to_speed) {
// Only keep a cut that changes the G-code: one that prints differently from at least one of the
// points either side of it, which are the line's ends or the other cut where that is kept. A cut
// that prints like both would only split a move into two identical ones.
if (keep[0])
keep[0] = !same_speed_and_fan(cut[0].distance, curr.distance) ||
!same_speed_and_fan(cut[0].distance, keep[1] ? cut[1].distance : next.distance);
if (keep[1])
keep[1] = !same_speed_and_fan(cut[1].distance, keep[0] ? cut[0].distance : curr.distance) ||
!same_speed_and_fan(cut[1].distance, next.distance);
// Two cuts closer together than min_spacing would leave a micro segment between them, so only the
// first is kept.
if (keep[0] && keep[1] && (cut[1].position - cut[0].position).norm() <= min_spacing)
keep[1] = false;
}
for (int k = 0; k < 2; ++k)
if (keep[k])
new_points.push_back(cut[k]);
}
}
new_points.push_back(next);
@@ -423,7 +447,10 @@ public:
const ConfigOptionFloatsOrPercents &speeds,
float ext_perimeter_speed,
float original_speed,
bool slowdown_for_curled_edges)
bool slowdown_for_curled_edges,
// Overlap at or below which the overhang fan switches on; negative when the fan
// does not depend on overlap.
float fan_overlap_threshold = -1.0f)
{
size_t speed_sections_count = std::min(overlaps.values.size(), speeds.values.size());
std::vector<std::pair<float, float>> speed_sections;
@@ -463,7 +490,8 @@ public:
}
}
// If a meaningful (i.e. needing slowdown) overhang distance was not found, then we shouldn't split the lines
// If no overhang distance slows this path down, -1 turns interior sampling off unless the overhang fan can switch.
// Lines are only split where an end prints slower or cooled, so here only a fan switch splits them.
if (!found)
smallest_distance_with_lower_speed=-1.f;
@@ -487,9 +515,17 @@ public:
return round(final_speed);
};
// ORCA: The speed sections are built from ext_perimeter_speed, which can be above the speed this path prints at
// (original_speed, e.g. held down by resonance avoidance). Every segment is capped at original_speed below, so
// overhang distances whose speeds differ only above it print the same and must not count as a speed change when
// the path is split.
auto effective_speed = [&calculate_speed, original_speed](float distance) {
return std::min(calculate_speed(distance), original_speed);
};
std::vector<ExtendedPoint<3>> extended_points =
estimate_points_properties<true, true, true, true>(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1,
smallest_distance_with_lower_speed, calculate_speed);
smallest_distance_with_lower_speed, effective_speed, fan_overlap_threshold);
const auto width_inv = 1.0f / path.width;
std::vector<ProcessedPoint> processed_points;
processed_points.reserve(extended_points.size());
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
#ifndef slic3r_PreciseSeam_hpp_
#define slic3r_PreciseSeam_hpp_
#include <atomic>
#include <optional>
#include <vector>
#include <unordered_map>
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "SeamPlacer.hpp"
// CURRENT STATUS:
// Strong modifiers (Center/Left/Right): only one intersection per perimeter is supported,
// since there can be only one seam. Additional intersections are ignored.
//
// Weak modifiers (Enforced/Blocked/Neutral): multiple intersections are supported,
// but none of them should pass through the model entirely. A through-body intersection
// produces multiple segments, of which only one will be processed.
//
// In both cases, a pop-up warning is shown when unsupported intersections are detected.
//
// If any modifier has a multiply-connected cross-section (e.g. a hollow shape),
// it is skipped and a corresponding notification is shown.
//
// Full containment of the perimeter within modifier is not handled.
//
// FUTURE DIRECTION:
// A lightweight algorithm is needed to detect and handle through-body intersections
// for Weak modifiers. The algorithm must not slow down the 99.9% common case.
// Possible approach: if intersection passes the diff check (no through-body),
// use the current fast algorithm. If diff check fails, fall back to a heavier
// method: compute midpoints of intersection polygon edges, then check which
// midpoints lie strictly inside the modifier (not on boundary) using
// point_in_polygon. Those edges originate from the perimeter; the rest
// originate from the modifier boundary. Collect perimeter edges into a polyline.
// Additionally, multiply-connected cross-sections could be supported instead of
// being skipped entirely (e.g. by decomposing them into simple polygons).
// For Enforced and Neutral weak modifiers, full containment of the perimeter
// within modifier could be handled (currently ignored).
namespace Slic3r {
namespace PreciseSeam {
// Import EnforcedBlockedSeamPoint from SeamPlacerImpl namespace for convenience
using SeamPlacerImpl::EnforcedBlockedSeamPoint;
// Pre-sliced modifier cache: ModelVolume pointer → per-layer Polygons.
// Built once in SeamPlacer::init(), then passed read-only into per-perimeter functions.
using ModifierSlicesCache = std::unordered_map<const ModelVolume*, std::vector<Polygons>>;
// Warning flags set during Precise Seam processing (thread-safe)
struct PreciseSeamWarnings {
std::atomic<bool> multiple_intersections{false}; // modifier intersects perimeter in multiple separate places (strong only)
std::atomic<bool> through_body{false}; // modifier passes through the model body entirely
std::atomic<bool> multiply_connected{false}; // modifier has holes (multiply-connected cross-section)
std::atomic<bool> full_containment{false}; // modifier fully contains perimeter, no intersection edges
};
// Result of finding common segment between perimeter and intersection
struct SegmentData {
Polyline segment; // Points from intersection_polygon forming the segment
std::vector<size_t> perimeter_edge_indices; // edge_index for each point in segment
};
// Result of weak modifier segment processing
struct WeakModifierSegment {
EnforcedBlockedSeamPoint type; // Enforced/Blocked/Neutral
Point left_point; // Coordinates of left (first) point of segment
size_t left_idx; // Perimeter vertex index for left_point
Point right_point; // Coordinates of right (last) point of segment
size_t right_idx; // Perimeter vertex index for right_point
};
// Initialize Precise Seam data by populating provided vectors and flag
// Collects precise seam modifiers and fills output parameters
// Call once during SeamPlacer::init() before gather_seam_candidates()
// Parameters:
// strong_volumes_out - output vector for strong modifiers (CENTER/LEFT/RIGHT)
// weak_volumes_out - output vector for weak modifiers (ENFORCED/BLOCKED/NEUTRAL)
// has_strong_out - output flag indicating presence of strong modifiers
// model_object - model object containing volumes
void init_precise_seam_data(
std::vector<const ModelVolume*>& strong_volumes_out,
std::vector<const ModelVolume*>& weak_volumes_out,
bool& has_strong_out,
const ModelObject* model_object);
// Insert strong seam point into perimeter polygon
// Processes strong modifiers (CENTER/LEFT/RIGHT) and inserts seam point into polygon
// Parameters:
// strong_volumes - list of strong precise seam modifiers
// polygon - perimeter polygon (will be modified if point inserted)
// layer - current layer
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
// Returns:
// Coordinates of inserted point (internal units) or std::nullopt if nothing inserted
std::optional<Point> insert_strong_seam_point(
const std::vector<const ModelVolume*> &strong_volumes,
Polygon &polygon,
const Layer *layer,
const ModifierSlicesCache &slices_cache,
PreciseSeamWarnings* warnings = nullptr);
// Collect all weak modifier segments for a perimeter polygon
// Processes weak modifiers (ENFORCED/BLOCKED/NEUTRAL) and collects segment boundaries
// Also inserts boundary points into the perimeter polygon (sorted by descending arc length)
// Refines enforced edges by subdividing them into segments ≤ enforcer_oversampling_distance
// Parameters:
// weak_volumes - list of weak precise seam modifiers
// polygon - perimeter polygon (will be modified with inserted points and refined edges)
// layer - current layer
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
// Returns:
// Ordered vector of segments with updated coordinates (same order as weak_volumes list)
std::vector<WeakModifierSegment> collect_weak_modifier_segments(
const std::vector<const ModelVolume*> &weak_volumes,
Polygon &polygon,
const Layer *layer,
const ModifierSlicesCache &slices_cache,
PreciseSeamWarnings* warnings = nullptr);
// Apply weak modifier types to perimeter points based on segment boundaries
// Finds boundary points in refined polygon and sets types for points within segments
// Parameters:
// weak_segments - segments with boundary coordinates and types
// result - layer seams data to modify
// perimeter - perimeter info (start/end indices)
// some_point_enforced - flag to update if Enforced points are set
void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
const SeamPlacerImpl::Perimeter &perimeter,
bool &some_point_enforced);
// Restore precise seam positions that may have been modified by alignment
// Iterates through all perimeters and restores precise_seam_point positions
void restore_precise_seam_positions(std::vector<PrintObjectSeamData::LayerSeams> &layers);
} // namespace PreciseSeam
} // namespace Slic3r
#endif // slic3r_PreciseSeam_hpp_
+160 -42
View File
@@ -10,6 +10,7 @@
#include <algorithm>
#include <limits>
#include <queue>
#include <unordered_map>
#include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/KDTreeIndirect.hpp"
@@ -20,10 +21,12 @@
#include "libslic3r/Layer.hpp"
#include "libslic3r/Geometry/Curves.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/ShortEdgeCollapse.hpp"
#include "libslic3r/TriangleSetSampling.hpp"
#include "libslic3r/Utils.hpp"
#include "PreciseSeam.hpp"
//#define DEBUG_FILES
@@ -304,6 +307,16 @@ struct GlobalModelInfo {
AABBTreeIndirect::Tree<3, float> enforcers_tree;
AABBTreeIndirect::Tree<3, float> blockers_tree;
// Precise Seam modifiers: strong modifiers (CENTER/LEFT/RIGHT) determine exact seam placement
std::vector<const ModelVolume*> precise_seam_strong_volumes;
// Precise Seam modifiers: weak modifiers (ENFORCED/BLOCKED/NEUTRAL) provide hints for seam placement
std::vector<const ModelVolume*> precise_seam_weak_volumes;
// Pre-sliced modifier polygons, keyed by ModelVolume pointer.
// Populated once in SeamPlacer::init() to avoid re-slicing on every perimeter.
// Each value is a per-layer vector of Polygons for that modifier volume.
std::unordered_map<const ModelVolume*, std::vector<Polygons>> precise_seam_slices;
bool is_enforced(const Vec3f &position, float radius) const {
if (enforcers.empty()) {
return false;
@@ -404,7 +417,8 @@ struct GlobalModelInfo {
;
//Extract perimeter polygons of the given layer
Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerRegion*> &corresponding_regions_out) {
Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerRegion*> &corresponding_regions_out,
bool has_precise_seam_modifiers) {
Polygons polygons;
for (const LayerRegion *layer_region : layer->regions()) {
for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) {
@@ -441,6 +455,18 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerR
}
}
if (has_precise_seam_modifiers) {
// Extrusion loops repeat their start point; Polygon closes the contour implicitly.
// Normalize here for Precise Seam without changing ordinary seam candidates.
for (Polygon &polygon : polygons) {
// Adjacent extrusion paths share endpoints; zero-length edges would prevent refinement at their junctions.
// Remove only consecutive duplicates, preserving distinct visits to a self-touching contour point.
polygon.points.erase(std::unique(polygon.points.begin(), polygon.points.end()), polygon.points.end());
while (polygon.size() > 1 && polygon.points.front() == polygon.points.back())
polygon.points.pop_back();
}
}
if (polygons.empty()) { // If there are no perimeter polygons for whatever reason (disabled perimeters .. ) insert dummy point
// it is easier than checking everywhere if the layer is not emtpy, no seam will be placed to this layer anyway
polygons.emplace_back(Points{ { 0, 0 } });
@@ -450,17 +476,42 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerR
return polygons;
}
// Insert SeamCandidates created from perimeter polygons in to the result vector.
// Compute its type (Enfrocer,Blocker), angle, and position
//each SeamCandidate also contains pointer to shared Perimeter structure representing the polygon
// if Custom Seam modifiers are present, oversamples the polygon if necessary to better fit user intentions
// Build SeamCandidates for each vertex of the perimeter polygon and attach them to a shared Perimeter.
// For each vertex: computes position, angle, and type (Enforcer / Blocker / Neutral).
// When Precise Seam modifiers are present: inserts strong seam point,
// oversamples enforcer edges, applies weak modifiers, marks one enforced point as central for alignment.
void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const LayerRegion *region,
const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result) {
const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result,
PreciseSeam::PreciseSeamWarnings* warnings = nullptr) {
if (orig_polygon.size() == 0) {
return;
}
Polygon polygon = orig_polygon;
bool was_clockwise = polygon.make_counter_clockwise();
// Process Precise Seam modifiers to find seam placement
const Layer* layer = region ? region->layer() : nullptr;
// Use pre-computed Precise Seam volumes from global_model_info (computed once in init)
const auto& strong_volumes = global_model_info.precise_seam_strong_volumes;
const auto& weak_volumes = global_model_info.precise_seam_weak_volumes;
// Use pre-sliced cache from global_model_info instead of re-slicing on every call
auto seam_point = PreciseSeam::insert_strong_seam_point(strong_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
// Store the inserted point position for marking as central_enforcer later
std::optional<Vec3f> inserted_seam_position;
if (seam_point.has_value()) {
Vec2f unscaled_p = unscale(seam_point.value()).cast<float>();
inserted_seam_position = Vec3f(unscaled_p.x(), unscaled_p.y(), z_coord);
}
// Process weak modifiers (ENFORCED/BLOCKED/NEUTRAL) only if no strong modifier was inserted
std::vector<PreciseSeam::WeakModifierSegment> weak_segments;
if (!inserted_seam_position.has_value()) {
weak_segments = PreciseSeam::collect_weak_modifier_segments(weak_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
}
float angle_arm_len = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).nozzle_diameter() : 0.5f;
std::vector<float> lengths { };
@@ -529,10 +580,15 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
perimeter.end_index = result.points.size();
// Apply weak modifiers if no strong modifier was inserted
if (!inserted_seam_position.has_value() && !weak_segments.empty()) {
PreciseSeam::apply_weak_modifiers_to_perimeter(
weak_segments, result, perimeter, some_point_enforced);
}
if (some_point_enforced) {
// We will patches of enforced points (patch: continuous section of enforced points), choose
// the longest patch, and select the middle point or sharp point (depending on the angle)
// this point will have high priority on this perimeter
// Choose the continuous enforced patch with the most candidates, then select its middle
// candidate or a sharp corner. Patch length here is a point count, not geometric distance.
size_t perimeter_size = perimeter.end_index - perimeter.start_index;
const auto next_index = [&](size_t idx) {
return perimeter.start_index + Slic3r::next_idx_modulo(idx - perimeter.start_index, perimeter_size);
@@ -563,7 +619,9 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
std::pair<size_t, size_t> longest_patch { 0, 0 };
auto patch_len = [perimeter_size](const std::pair<size_t, size_t> &start_end) {
if (start_end.second < start_end.first) {
return start_end.first + (perimeter_size - start_end.second);
// Count [start, end) across the closing edge, independently of the contour's start.
// Subtract indices first: they are offsets in the layer, not local perimeter indices.
return perimeter_size - (start_end.first - start_end.second);
} else {
return start_end.second - start_end.first;
}
@@ -596,30 +654,24 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
}
}
}
// Get index of previous and next perimeter point of the layer. Because SeamCandidates of all polygons of the given layer
// are sequentially stored in the vector, each perimeter contains info about start and end index. These vales are used to
// deduce index of previous and next neigbour in the corresponding perimeter.
std::pair<size_t, size_t> find_previous_and_next_perimeter_point(const std::vector<SeamCandidate> &perimeter_points,
size_t point_index) {
const SeamCandidate &current = perimeter_points[point_index];
int prev = point_index - 1; //for majority of points, it is true that neighbours lie behind and in front of them in the vector
int next = point_index + 1;
if (point_index == current.perimeter.start_index) {
// if point_index is equal to start, it means that the previous neighbour is at the end
prev = current.perimeter.end_index;
// Apply precise seam point if it was inserted
if (inserted_seam_position.has_value()) {
// Set single point as Enforced, block all others
for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) {
if (result.points[i].position == inserted_seam_position.value()) {
// Mark as the single enforced point with highest priority
result.points[i].type = EnforcedBlockedSeamPoint::Enforced;
result.points[i].central_enforcer = true;
perimeter.precise_seam_point = inserted_seam_position;
perimeter.precise_seam_index = i;
} else {
// Block all other points
result.points[i].type = EnforcedBlockedSeamPoint::Blocked;
result.points[i].central_enforcer = false;
}
}
}
if (point_index == current.perimeter.end_index - 1) {
// if point_index is equal to end, than next neighbour is at the start
next = current.perimeter.start_index;
}
assert(prev >= 0);
assert(next >= 0);
return {size_t(prev),size_t(next)};
}
// Computes all global model info - transforms object, performs raycasting
@@ -637,7 +689,9 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
|| model_volume->type() == ModelVolumeType::NEGATIVE_VOLUME) {
auto model_transformation = model_volume->get_matrix();
indexed_triangle_set model_its = model_volume->mesh().its;
// ORCA: Mirrored transforms flip winding, keep normals outward
// ORCA fix (not related to Precise Seam, discovered during its development):
// Mirror transforms have negative determinant which flips triangle winding.
// fix_left_handed=true swaps indices to keep normals pointing outward.
its_transform(model_its, model_transformation, true);
if (model_volume->type() == ModelVolumeType::MODEL_PART) {
its_merge(triangle_set, model_its);
@@ -658,7 +712,10 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
size_t negative_volumes_start_index = triangle_set.indices.size();
its_merge(triangle_set, negative_volumes_set);
// ORCA: Mirroring flips normals, keep them outward for visibility sampling
// ORCA fix (not related to Precise Seam, discovered during its development):
// Object-level transform may include mirroring (negative determinant),
// which inverts triangle winding. fix_left_handed=true corrects this
// so visibility ray sampling sees outward-facing normals.
its_transform(triangle_set, obj_transform, true);
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: decimate: end";
@@ -720,12 +777,12 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
auto model_transformation = obj_transform * mv->get_matrix();
indexed_triangle_set enforcers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER);
// ORCA: Keep normals outward when mirroring seam enforcers
// ORCA fix (not related to Precise Seam): fix winding for mirrored transforms
its_transform(enforcers, model_transformation, true);
its_merge(result.enforcers, enforcers);
indexed_triangle_set blockers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::BLOCKER);
// ORCA: Keep normals outward when mirroring seam blockers
// ORCA fix (not related to Precise Seam): fix winding for mirrored transforms
its_transform(blockers, model_transformation, true);
its_merge(result.blockers, blockers);
}
@@ -1012,13 +1069,14 @@ void pick_random_seam_point(const std::vector<SeamCandidate> &perimeter_points,
// Parallel process and extract each perimeter polygon of the given print object.
// Gather SeamCandidates of each layer into vector and build KDtree over them
// Store results in the SeamPlacer variables m_seam_per_object
void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info) {
void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
PreciseSeam::PreciseSeamWarnings* warnings) {
using namespace SeamPlacerImpl;
PrintObjectSeamData &seam_data = m_seam_per_object.emplace(po, PrintObjectSeamData { }).first->second;
seam_data.layers.resize(po->layer_count());
tbb::parallel_for(tbb::blocked_range<size_t>(0, po->layers().size()),
[po, &global_model_info, &seam_data]
[po, &global_model_info, &seam_data, warnings]
(tbb::blocked_range<size_t> r) {
for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) {
PrintObjectSeamData::LayerSeams &layer_seams = seam_data.layers[layer_idx];
@@ -1026,10 +1084,13 @@ void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerI
auto unscaled_z = layer->slice_z;
std::vector<const LayerRegion*> regions;
//NOTE corresponding region ptr may be null, if the layer has zero perimeters
Polygons polygons = extract_perimeter_polygons(layer, regions);
const bool has_precise_seam_modifiers = !global_model_info.precise_seam_strong_volumes.empty() ||
!global_model_info.precise_seam_weak_volumes.empty();
Polygons polygons = extract_perimeter_polygons(layer, regions, has_precise_seam_modifiers);
for (size_t poly_index = 0; poly_index < polygons.size(); ++poly_index) {
process_perimeter_polygon(polygons[poly_index], unscaled_z,
regions[poly_index], global_model_info, layer_seams);
regions[poly_index], global_model_info, layer_seams,
warnings);
}
auto functor = SeamCandidateCoordinateFunctor { layer_seams.points };
seam_data.layers[layer_idx].points_tree =
@@ -1429,10 +1490,13 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const SeamPlacerImpl::
}
void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_canceled_func) {
void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_func) {
using namespace SeamPlacerImpl;
m_seam_per_object.clear();
// Warning flags for Precise Seam processing — shared across all objects
PreciseSeam::PreciseSeamWarnings precise_seam_warnings;
for (const PrintObject *po : print.objects()) {
throw_if_canceled_func();
SeamPosition configured_seam_preference = po->config().seam_position.value;
@@ -1441,14 +1505,29 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
{
GlobalModelInfo global_model_info { };
gather_enforcers_blockers(global_model_info, po);
PreciseSeam::init_precise_seam_data(
global_model_info.precise_seam_strong_volumes,
global_model_info.precise_seam_weak_volumes,
m_seam_per_object[po].has_precise_seam_strong_volumes,
po->model_object());
// Pre-slice all precise seam modifier volumes once per object.
// Without this cache, slice_single_volume() would be called for every
// modifier × every perimeter × every layer — thousands of redundant slicing operations.
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
throw_if_canceled_func();
if (configured_seam_preference == spAligned || configured_seam_preference == spNearest || configured_seam_preference == spAlignedBack) {
compute_global_occlusion(global_model_info, po, throw_if_canceled_func, configured_seam_preference);
}
throw_if_canceled_func();
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather_seam_candidates: start";
gather_seam_candidates(po, global_model_info);
gather_seam_candidates(po, global_model_info, &precise_seam_warnings);
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather_seam_candidates: end";
throw_if_canceled_func();
@@ -1496,10 +1575,49 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
<< "SeamPlacer: align_seam_points : end";
}
// Restore precise seam positions that were potentially modified
if (m_seam_per_object[po].has_precise_seam_strong_volumes) {
PreciseSeam::restore_precise_seam_positions(m_seam_per_object[po].layers);
}
#ifdef DEBUG_FILES
debug_export_points(m_seam_per_object[po].layers, po->bounding_box(), comparator);
#endif
}
// Show Precise Seam warnings (once for all objects).
// Only ONE active_step_add_warning() call — multiple calls generate multiple UI events,
// each re-pushing ALL current warnings via Plater handler, causing NotificationManager::append()
// to duplicate text within each popup.
{
const bool mi = precise_seam_warnings.multiple_intersections.load(std::memory_order_relaxed);
const bool tb = precise_seam_warnings.through_body.load(std::memory_order_relaxed);
const bool mc = precise_seam_warnings.multiply_connected.load(std::memory_order_relaxed);
const bool fc = precise_seam_warnings.full_containment.load(std::memory_order_relaxed);
std::vector<std::string> parts;
if (mi)
parts.push_back(_u8L("multiple intersections with a perimeter detected"));
if (tb)
parts.push_back(_u8L("modifier fully crosses the printable perimeter"));
if (mc)
parts.push_back(_u8L("modifier shape is not solid (has holes inside) and was ignored"));
if (fc)
parts.push_back(_u8L("perimeter is fully contained inside modifier and was ignored"));
if (!parts.empty()) {
// One line: the export warnings dialog shows only the first line of each warning.
std::string warning_text = _u8L("Precise Seam") + ": ";
for (size_t i = 0; i < parts.size(); ++i) {
if (i > 0) warning_text += "; ";
warning_text += parts[i];
}
warning_text += ". ";
warning_text += _u8L("Seam placement may differ from expected.");
print.active_step_add_warning(
PrintStateBase::WarningLevel::NON_CRITICAL,
warning_text,
PrintStateBase::SlicingPreciseSeamWarning);
}
}
}
void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop,
+12 -3
View File
@@ -18,6 +18,7 @@
namespace Slic3r {
class PrintObject;
namespace PreciseSeam { struct PreciseSeamWarnings; }
class ExtrusionLoop;
class Print;
class Layer;
@@ -41,7 +42,7 @@ enum class EnforcedBlockedSeamPoint {
// struct representing single perimeter loop
struct Perimeter {
size_t start_index{};
size_t end_index{}; //inclusive!
size_t end_index{}; //exclusive (one-past-the-end)
size_t seam_index{};
float flow_width{};
@@ -50,6 +51,10 @@ struct Perimeter {
// Random position also uses this flexibility to set final seam point position
bool finalized = false;
Vec3f final_seam_position = Vec3f::Zero();
// Stores precise seam coordinates found by Precise Seam modifiers
std::optional<Vec3f> precise_seam_point;
size_t precise_seam_index{};
};
//Struct over which all processing of perimeters is done. For each perimeter point, its respective candidate is created,
@@ -102,6 +107,9 @@ struct PrintObjectSeamData
// Map of PrintObjects (PO) -> vector of layers of PO -> unique_ptr to KD
// tree of all points of the given layer
// Indicates presence of strong Precise Seam modifiers (CENTER/LEFT/RIGHT) for this object
bool has_precise_seam_strong_volumes = false;
void clear()
{
layers.clear();
@@ -141,11 +149,12 @@ public:
//The following data structures hold all perimeter points for all PrintObject.
std::unordered_map<const PrintObject*, PrintObjectSeamData> m_seam_per_object;
void init(const Print &print, std::function<void(void)> throw_if_canceled_func);
void init(Print &print, std::function<void(void)> throw_if_canceled_func);
void place_seam(const Layer *layer, ExtrusionLoop &loop, const Point &last_pos, float& overhang) const;
private:
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info);
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
PreciseSeam::PreciseSeamWarnings* warnings = nullptr);
void calculate_candidates_visibility(const PrintObject *po,
const SeamPlacerImpl::GlobalModelInfo &global_model_info);
void calculate_overhangs_and_layer_embedding(const PrintObject *po);
+73 -5
View File
@@ -49,6 +49,48 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
return tower_z;
}
bool wipe_tower_sparse_layers_combined(const PrintConfig &config)
{
return config.wipe_tower_sparse_layers_combination.value && ! wipe_tower_sparse_layers_skipped(config) &&
config.timelapse_type.value != TimelapseType::tlSmooth && ! config.enable_wrapping_detection.value;
}
bool wipe_tower_layer_is_combined_away(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
{
return ! layer_tool_changes.empty() && layer_tool_changes.front().combined_away;
}
std::vector<char> combine_sparse_wipe_tower_layers(std::vector<float> &layer_height,
const std::vector<char> &layer_is_sparse,
const std::vector<float> &max_layer_height,
size_t first_layer_idx)
{
assert(layer_is_sparse.size() == layer_height.size() && max_layer_height.size() == layer_height.size());
std::vector<char> combined_away(layer_height.size(), 0);
float pending_height = 0.f; // what the layers folded away so far add up to
for (size_t i = 0; i < layer_height.size(); ++i) {
// A toolchange has to purge at its own z, so it neither folds away nor takes over the run
// below it - and a run always flushes on its own last layer, so nothing is ever pending here.
if (! layer_is_sparse[i] || i <= first_layer_idx) {
pending_height = 0.f;
continue;
}
const float merged = pending_height + layer_height[i];
// Hand the run on only if the next layer can swallow the whole thing; a layer already past
// the cap is left alone rather than shrunk.
const bool next_takes_it = i + 1 < layer_height.size() && layer_is_sparse[i + 1] &&
merged + layer_height[i + 1] <= max_layer_height[i + 1] + float(EPSILON);
if (next_takes_it) {
combined_away[i] = 1;
pending_height = merged;
} else {
layer_height[i] = merged;
pending_height = 0.f;
}
}
return combined_away;
}
inline float align_round(float value, float base)
{
return std::round(value / base) * base;
@@ -1904,6 +1946,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
//m_bridging(float(config.wipe_tower_bridging)),
m_bridging(10.f),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)),
m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_current_tool(initial_tool),
@@ -2028,6 +2071,16 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
float nozzle_diameter = float(config.nozzle_diameter.get_at(idx));
m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM
// Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than
// by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp.
{
const std::vector<int> &filament_map = config.filament_map.values; // 1 based nozzle indices
const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0;
const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx));
m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height
: 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx));
}
float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx));
if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
@@ -3001,7 +3054,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_sparse_layers_skipped || toolchanges_on_layer)
if (layer_is_printed(toolchanges_on_layer))
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3898,7 +3951,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (layer_is_printed(toolchanges_on_layer))
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -4008,7 +4061,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (layer_is_printed(toolchanges_on_layer))
if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4125,7 +4178,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (layer_is_printed(toolchanges_on_layer))
if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4668,6 +4721,15 @@ void WipeTower::calc_block_infill_gap()
m_extra_spacing = 1.f;
}
// A folded layer is generated like any other but thrown away by the emitter, so its extrusions must
// not be charged to the filament used.
bool WipeTower::layer_is_printed(bool toolchanges_on_layer) const
{
if (m_layer_info != m_plan.end() && m_layer_info->combined_away)
return false;
return ! m_sparse_layers_skipped || toolchanges_on_layer;
}
void WipeTower::plan_tower_new()
{
if (m_wipe_tower_brim_width < 0) m_wipe_tower_brim_width = get_auto_brim_by_height(m_wipe_tower_height);
@@ -4820,6 +4882,9 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
if (m_plan.empty())
return;
//m_extra_spacing = 1.f;
// Before planning: the layer heights this rewrites feed the extrusion flow of every later pass.
if (m_sparse_layers_combined)
combine_sparse_wipe_tower_plan(m_plan, m_filpar, m_first_layer_idx, m_current_tool);
m_wipe_tower_height = m_plan.back().z;//real wipe_tower_height
plan_tower_new();
m_layer_info = m_plan.begin();
@@ -5014,6 +5079,9 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
if (only_generate_wall && !timelapse_wall.gcode.empty()) {
layer_result.insert(layer_result.begin(), std::move(timelapse_wall));
}
if (layer.combined_away)
for (WipeTower::ToolChangeResult &tcr : layer_result)
tcr.combined_away = true;
result.emplace_back(std::move(layer_result));
}
assert(m_outer_wall.size() == m_plan.size());
@@ -5179,7 +5247,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (layer_is_printed(toolchanges_on_layer))
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
+67
View File
@@ -152,6 +152,10 @@ public:
bool is_contact = false;
NozzleChangeResult nozzle_change_result;
// Orca: folded into a later, thicker layer, so the emitter drops it. Set by the tower, so
// the two cannot disagree about which layers print.
bool combined_away = false;
// Sum the total length of the extrusion.
float total_extrusion_length_in_plane() {
float e_length = 0.f;
@@ -391,6 +395,8 @@ public:
float filament_tower_interface_pre_extrusion_dist = 0;
float filament_tower_interface_pre_extrusion_length = 0;
float filament_petg_pre_extrusion_offset_dist = 0;
// Tallest layer this filament's nozzle can lay down; caps the sparse layer combination.
float max_layer_height = 0.f;
};
@@ -522,6 +528,7 @@ private:
//float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_sparse_layers_skipped = false;
bool m_sparse_layers_combined = false;
// BBS: remove useless config
//bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
@@ -595,6 +602,8 @@ private:
}
// Calculates depth for all layers and propagates them downwards
void plan_tower();
// Whether the layer reaches the G-code, and so whether its extrusions count as filament used.
bool layer_is_printed(bool toolchanges_on_layer) const;
// Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental
void make_wipe_tower_square();
@@ -634,6 +643,8 @@ private:
float depth; // depth of the layer based on all layers above
float extra_spacing;
bool extruder_fill{true};
// Folded into a later, thicker layer, so this one prints nothing at all.
bool combined_away{false};
float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; }
std::vector<ToolChange> tool_changes;
@@ -700,6 +711,62 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
float base_z = 0.f);
// Combination rule for wipe_tower_sparse_layers_combination. Nothing is compacted - the tower keeps
// following the object - but a run of consecutive toolchange-free layers prints as one thicker layer,
// the way infill combination merges sparse infill. Shared so that neither tower generator nor the
// G-code emitter can combine on its own.
// Whether sparse layers are really combined. Skipping them outright is the stronger answer to the
// same problem and wins over this; smooth timelapse and wrapping detection need a tower on every
// layer, so they rule it out too.
bool wipe_tower_sparse_layers_combined(const PrintConfig &config);
// A planned layer folded into a later, thicker one prints nothing at all.
bool wipe_tower_layer_is_combined_away(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
// Folds runs of sparse layers into one. layer_height is raised in place on the layer that prints a
// run - always its last, so the merged extrusion lands on top of what it covers - and the returned
// mask marks the layers that now print nothing. A run stops growing once one more layer would pass
// max_layer_height of the nozzle that prints it. first_layer_idx and below never combine: the
// tower's first layer carries the brim.
std::vector<char> combine_sparse_wipe_tower_layers(std::vector<float> &layer_height,
const std::vector<char> &layer_is_sparse,
const std::vector<float> &max_layer_height,
size_t first_layer_idx);
// Applies the rule above to a planned tower. Either generator's plan fits: both carry height,
// tool_changes and combined_away per layer, and index their filament parameters by tool.
template<class PlanLayers, class FilamentParams>
void combine_sparse_wipe_tower_plan(PlanLayers &plan, const FilamentParams &filpar, size_t first_layer_idx, size_t initial_tool)
{
const size_t n = plan.size();
std::vector<float> heights(n);
std::vector<char> sparse(n);
std::vector<float> caps(n);
// A layer with no toolchange prints with the filament the layer below left loaded.
size_t tool = initial_tool;
for (const auto &layer : plan)
if (! layer.tool_changes.empty()) {
tool = layer.tool_changes.front().old_tool;
break;
}
for (size_t i = 0; i < n; ++i) {
heights[i] = plan[i].height;
sparse[i] = plan[i].tool_changes.empty() ? 1 : 0;
caps[i] = tool < filpar.size() ? filpar[tool].max_layer_height : 0.f;
if (! plan[i].tool_changes.empty())
tool = plan[i].tool_changes.back().new_tool;
}
const std::vector<char> combined_away = combine_sparse_wipe_tower_layers(heights, sparse, caps, first_layer_idx);
for (size_t i = 0; i < n; ++i) {
plan[i].height = heights[i];
plan[i].combined_away = combined_away[i] != 0;
}
}
} // namespace Slic3r
#endif // WipeTowerPrusaMM_hpp_
+22 -1
View File
@@ -1033,6 +1033,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)),
m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
@@ -1150,6 +1151,16 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config)
float nozzle_diameter = float(config.nozzle_diameter.get_at(idx));
m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM
// Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than
// by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp.
{
const std::vector<int> &filament_map = config.filament_map.values; // 1 based nozzle indices
const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0;
const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx));
m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height
: 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx));
}
float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx));
if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
@@ -2103,7 +2114,9 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) {
// A folded layer prints nothing, so it consumes nothing and adds no height of its own.
const bool combined_away = m_layer_info != m_plan.end() && m_layer_info->combined_away;
if ((! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) && ! combined_away) {
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
m_current_height += m_layer_info->height;
@@ -2435,6 +2448,10 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
if (m_plan.empty())
return;
// Before planning: the layer heights this rewrites feed the extrusion flow of every later pass.
if (m_sparse_layers_combined)
combine_sparse_wipe_tower_plan(m_plan, m_filpar, m_first_layer_idx, m_current_tool);
plan_tower();
#if 1
for (int i=0;i<5;++i) {
@@ -2533,6 +2550,10 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
layer_result[idx] = merge_tcr(layer_result[idx], finish_layer_tcr);
}
if (layer.combined_away)
for (WipeTower::ToolChangeResult &tcr : layer_result)
tcr.combined_away = true;
result.emplace_back(std::move(layer_result));
if (m_used_filament_length_until_layer.empty() || m_used_filament_length_until_layer.back().first != layer.z)
+5
View File
@@ -200,6 +200,8 @@ public:
float tower_interface_pre_extrusion_length = 0.f;
float tower_ironing_area = 4.f;
float tower_interface_purge_length = 0.f;
// Tallest layer this filament's nozzle can lay down; caps the sparse layer combination.
float max_layer_height = 0.f;
};
private:
@@ -268,6 +270,7 @@ private:
float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_sparse_layers_skipped = false;
bool m_sparse_layers_combined = false;
bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
@@ -368,6 +371,8 @@ private:
float z; // z position of the layer
float height; // layer height
float depth; // depth of the layer based on all layers above
// Folded into a later, thicker layer, so this one prints nothing at all.
bool combined_away{false};
float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; }
std::vector<ToolChange> tool_changes;
+249
View File
@@ -0,0 +1,249 @@
#pragma once
// LifecycleEvents.hpp
// --------------------
// Application lifecycle events (project, slicing, plate editing, preset, printer connection, and
// job activity) that other subsystems -- chiefly the plugin layer above libslic3r -- may want to
// observe. Lives in libslic3r rather than the plugin layer because some events fire from inside
// the slicing engine itself; see fire_lifecycle_event() below.
#include <functional>
#include <condition_variable>
#include <cstddef>
#include <mutex>
#include <string>
#include <utility>
namespace Slic3r
{
enum class LifecycleEvent {
// Project (3mf)
NewProject,
ProjectOpened,
ProjectBeforeSave,
ProjectAfterSave,
ProjectClosed,
ProjectDirtyChanged,
// Slicing pipeline
SliceStarted,
SliceGeometryFinished,
GCodeExportStarted,
GCodeExportFinished,
SlicingJobComplete,
// Plate/model editing
ObjectAdded,
ObjectDeleted,
ObjectTransformed,
ObjectChanged,
ObjectRenamed,
PlateCreated,
PlateDeleted,
PlateSelected,
PlateRenamed,
// Preset
PresetSelected,
PresetSaved,
// Printer/device
PrintStateChanged,
DeviceOnline,
DeviceOffline,
DeviceDiscovered,
DeviceSelected,
UploadStarted,
UploadFinished,
// Print/send jobs
PrintJobStarted,
PrintJobFinished,
SendJobStarted,
SendJobFinished,
};
// Scoped so callers must qualify (LifecycleEvtCode::Error, not ERROR) -- ERROR/OK collide with
// Windows macros (wingdi.h) as unqualified names.
enum class LifecycleEvtCode { Ok, Error, Warn };
struct LifecycleEventContext
{
// The primary subject identifier for the event. This is event-specific (for example, a
// project/output path, preset name, device id, or object name), must not contain status or
// prose, and may be empty when the event has no single subject.
std::string name;
// Outcome of the operation represented by the event. For state-change and start events,
// Ok means that the event occurred; it does not imply that a future operation succeeded.
LifecycleEvtCode code = LifecycleEvtCode::Ok;
// Optional human-readable detail or diagnostic text. It is not a stable parsing contract;
// machine-readable data should be represented by a dedicated field or event instead.
std::string msg;
// Stable subject/object identifier, when the source model provides one.
std::string id;
// Previous value for rename and other before/after events.
std::string previous_name;
// Device identifier for printer and job events.
std::string device_id;
// Job identifier when the originating queue/task provides one.
std::string job_id;
// Source subsystem or operation detail, suitable for filtering but not guaranteed to be
// exhaustive across versions.
std::string source;
// Plate, object, or volume index when the source uses an index rather than a stable id.
int index = -1;
// Aggregate project dirty state for ProjectDirtyChanged.
bool dirty = false;
// Optional host-side cancellation probe. Background slicing and G-code export events set
// this to the originating Print's cancellation state so dispatch can stop before calling
// the next capability. It is intentionally not exposed through the Python payload API.
std::function<bool()> cancellation_check;
};
inline std::string lifecycle_event_to_string(LifecycleEvent event)
{
switch (event) {
case LifecycleEvent::NewProject: return "NewProject";
case LifecycleEvent::ProjectOpened: return "ProjectOpened";
case LifecycleEvent::ProjectBeforeSave: return "ProjectBeforeSave";
case LifecycleEvent::ProjectAfterSave: return "ProjectAfterSave";
case LifecycleEvent::ProjectClosed: return "ProjectClosed";
case LifecycleEvent::ProjectDirtyChanged: return "ProjectDirtyChanged";
case LifecycleEvent::SliceStarted: return "SliceStarted";
case LifecycleEvent::SliceGeometryFinished: return "SliceGeometryFinished";
case LifecycleEvent::GCodeExportStarted: return "GCodeExportStarted";
case LifecycleEvent::GCodeExportFinished: return "GCodeExportFinished";
case LifecycleEvent::SlicingJobComplete: return "SlicingJobComplete";
case LifecycleEvent::ObjectAdded: return "ObjectAdded";
case LifecycleEvent::ObjectDeleted: return "ObjectDeleted";
case LifecycleEvent::ObjectTransformed: return "ObjectTransformed";
case LifecycleEvent::ObjectChanged: return "ObjectChanged";
case LifecycleEvent::ObjectRenamed: return "ObjectRenamed";
case LifecycleEvent::PlateCreated: return "PlateCreated";
case LifecycleEvent::PlateDeleted: return "PlateDeleted";
case LifecycleEvent::PlateSelected: return "PlateSelected";
case LifecycleEvent::PlateRenamed: return "PlateRenamed";
case LifecycleEvent::PresetSelected: return "PresetSelected";
case LifecycleEvent::PresetSaved: return "PresetSaved";
case LifecycleEvent::PrintStateChanged: return "PrintStateChanged";
case LifecycleEvent::DeviceOnline: return "DeviceOnline";
case LifecycleEvent::DeviceOffline: return "DeviceOffline";
case LifecycleEvent::DeviceDiscovered: return "DeviceDiscovered";
case LifecycleEvent::DeviceSelected: return "DeviceSelected";
case LifecycleEvent::UploadStarted: return "UploadStarted";
case LifecycleEvent::UploadFinished: return "UploadFinished";
case LifecycleEvent::PrintJobStarted: return "PrintJobStarted";
case LifecycleEvent::PrintJobFinished: return "PrintJobFinished";
case LifecycleEvent::SendJobStarted: return "SendJobStarted";
case LifecycleEvent::SendJobFinished: return "SendJobFinished";
default: return "Unknown";
}
}
inline std::string lifecycle_evt_code_to_string(LifecycleEvtCode code)
{
switch (code) {
case LifecycleEvtCode::Ok: return "Ok";
case LifecycleEvtCode::Error: return "Error";
case LifecycleEvtCode::Warn: return "Warn";
default: return "Unknown";
}
}
// Global cross-layer seam (mirrors ConfigBase::set_resolve_capability_fn): any libslic3r code can
// fire a lifecycle event without depending on the plugin layer above it, which installs the
// dispatcher here at startup. Not tied to Print/GCode specifically, since nothing here should
// require callers to hold a Print& just to report an event.
using LifecycleHookFn = std::function<void(LifecycleEvent, const LifecycleEventContext&)>;
namespace detail {
struct LifecycleHookState
{
std::mutex mutex;
std::condition_variable cv;
LifecycleHookFn fn;
std::size_t active_dispatches = 0;
bool accepting = false;
};
inline LifecycleHookState& lifecycle_hook_state()
{
static LifecycleHookState state;
return state;
}
class LifecycleDispatchGuard
{
public:
explicit LifecycleDispatchGuard(LifecycleHookState& state) : m_state(state) {}
~LifecycleDispatchGuard()
{
std::lock_guard<std::mutex> lock(m_state.mutex);
--m_state.active_dispatches;
if (m_state.active_dispatches == 0)
m_state.cv.notify_all();
}
LifecycleDispatchGuard(const LifecycleDispatchGuard&) = delete;
LifecycleDispatchGuard& operator=(const LifecycleDispatchGuard&) = delete;
private:
LifecycleHookState& m_state;
};
} // namespace detail
// Installing a hook starts accepting dispatches. Passing an empty function stops accepting
// new dispatches, detaches the hook, and waits for callbacks already in progress to finish.
// This is used during plugin shutdown so plugin code cannot be unloaded while a lifecycle
// callback is still executing. The empty-function path must not be called from inside the
// lifecycle callback itself.
inline void set_lifecycle_hook_fn(LifecycleHookFn fn)
{
detail::LifecycleHookState& state = detail::lifecycle_hook_state();
if (fn) {
std::lock_guard<std::mutex> lock(state.mutex);
state.fn = std::move(fn);
state.accepting = true;
return;
}
std::unique_lock<std::mutex> lock(state.mutex);
state.accepting = false;
state.fn = nullptr;
state.cv.wait(lock, [&state] { return state.active_dispatches == 0; });
}
inline void fire_lifecycle_event(LifecycleEvent event, const LifecycleEventContext& ctx)
{
detail::LifecycleHookState& state = detail::lifecycle_hook_state();
LifecycleHookFn fn;
{
std::lock_guard<std::mutex> lock(state.mutex);
if (!state.accepting || !state.fn)
return;
fn = state.fn;
++state.active_dispatches;
}
detail::LifecycleDispatchGuard guard(state);
fn(event, ctx);
}
}
+57 -4
View File
@@ -23,6 +23,7 @@
#include "libslic3r/Geometry/ConvexHull.hpp"
#include <algorithm>
#include <float.h>
#include <boost/algorithm/string/predicate.hpp>
@@ -1229,6 +1230,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
this->volumes.emplace_back(new ModelVolume(*model_volume));
this->volumes.back()->set_model_object(this);
}
this->clear_instances();
this->instances.reserve(rhs.instances.size());
for (const ModelInstance *model_instance : rhs.instances) {
@@ -1267,6 +1269,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
rhs.volumes.clear();
for (ModelVolume *model_volume : this->volumes)
model_volume->set_model_object(this);
this->clear_instances();
this->instances = std::move(rhs.instances);
rhs.instances.clear();
@@ -1390,7 +1393,9 @@ ModelVolume* ModelObject::add_volume_with_shared_mesh(const ModelVolume &other,
void ModelObject::delete_volume(size_t idx)
{
ModelVolumePtrs::iterator i = this->volumes.begin() + idx;
delete *i;
ModelVolume* volume_to_delete = *i;
delete volume_to_delete;
this->volumes.erase(i);
if (this->volumes.size() == 1)
@@ -1449,6 +1454,20 @@ void ModelObject::sort_volumes(bool full_sort)
// sort volumes inside the object to order "Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. "
if (full_sort)
std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) {
// Special handling for Precise Seam modifiers: group-based sorting with user order preservation
if (vl->is_precise_seam() && vr->is_precise_seam()) {
// Strong (center/left/right) always before weak (enforced/blocked/neutral)
bool vl_strong = vl->is_precise_seam_strong();
bool vr_strong = vr->is_precise_seam_strong();
if (vl_strong != vr_strong)
return vl_strong; // strong < weak → strong group appears first
// Within same group (both strong or both weak): preserve current order
// stable_sort will maintain relative positions when comparator returns false
return false;
}
// For non-Precise-Seam or mixed types: use standard enum-based ordering
return vl->type() < vr->type();
});
// sort have to controll "place" of the support blockers/enforcers. But one of the model parts have to be on the first place.
@@ -1456,10 +1475,19 @@ void ModelObject::sort_volumes(bool full_sort)
std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) {
ModelVolumeType vl_type = vl->type() > ModelVolumeType::PARAMETER_MODIFIER ? vl->type() : ModelVolumeType::PARAMETER_MODIFIER;
ModelVolumeType vr_type = vr->type() > ModelVolumeType::PARAMETER_MODIFIER ? vr->type() : ModelVolumeType::PARAMETER_MODIFIER;
// Apply same Precise Seam grouping logic for partial sort
if (vl->is_precise_seam() && vr->is_precise_seam()) {
bool vl_strong = vl->is_precise_seam_strong();
bool vr_strong = vr->is_precise_seam_strong();
if (vl_strong != vr_strong)
return vl_strong;
return false; // preserve order within same group
}
return vl_type < vr_type;
});
}
ModelInstance* ModelObject::add_instance()
{
ModelInstance* i = new ModelInstance(this);
@@ -2620,7 +2648,8 @@ std::vector<int> ModelVolume::get_extruders() const
if (m_type == ModelVolumeType::INVALID
|| m_type == ModelVolumeType::NEGATIVE_VOLUME
|| m_type == ModelVolumeType::SUPPORT_BLOCKER
|| m_type == ModelVolumeType::SUPPORT_ENFORCER)
|| m_type == ModelVolumeType::SUPPORT_ENFORCER
|| this->is_precise_seam()) // Precise Seam is non-printing helper geometry
return std::vector<int>();
if (mmu_segmentation_facets.timestamp() != mmuseg_ts) {
@@ -2824,6 +2853,19 @@ ModelVolumeType ModelVolume::type_from_string(const std::string &s)
return ModelVolumeType::SUPPORT_ENFORCER;
if (s == "support_blocker")
return ModelVolumeType::SUPPORT_BLOCKER;
// Precise Seam types
if (s == "precise_seam_center")
return ModelVolumeType::PRECISE_SEAM_CENTER;
if (s == "precise_seam_left")
return ModelVolumeType::PRECISE_SEAM_LEFT;
if (s == "precise_seam_right")
return ModelVolumeType::PRECISE_SEAM_RIGHT;
if (s == "precise_seam_enforced")
return ModelVolumeType::PRECISE_SEAM_ENFORCED;
if (s == "precise_seam_blocked")
return ModelVolumeType::PRECISE_SEAM_BLOCKED;
if (s == "precise_seam_neutral")
return ModelVolumeType::PRECISE_SEAM_NEUTRAL;
//assert(s == "0");
// Default value if invalud type string received.
return ModelVolumeType::MODEL_PART;
@@ -2838,6 +2880,12 @@ std::string ModelVolume::type_to_string(const ModelVolumeType t)
case ModelVolumeType::PARAMETER_MODIFIER: return "modifier_part";
case ModelVolumeType::SUPPORT_ENFORCER: return "support_enforcer";
case ModelVolumeType::SUPPORT_BLOCKER: return "support_blocker";
case ModelVolumeType::PRECISE_SEAM_CENTER: return "precise_seam_center";
case ModelVolumeType::PRECISE_SEAM_LEFT: return "precise_seam_left";
case ModelVolumeType::PRECISE_SEAM_RIGHT: return "precise_seam_right";
case ModelVolumeType::PRECISE_SEAM_ENFORCED: return "precise_seam_enforced";
case ModelVolumeType::PRECISE_SEAM_BLOCKED: return "precise_seam_blocked";
case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return "precise_seam_neutral";
default:
assert(false);
return "normal_part";
@@ -3712,7 +3760,11 @@ void FacetsAnnotation::set_triangle_from_string(int triangle_id, const std::stri
m_data.bitstream.insert(m_data.bitstream.end(), bool(dec & (1 << i)));
}
m_data.update_used_states(bitstream_start_idx);
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();
}
}
bool FacetsAnnotation::equals(const FacetsAnnotation &other) const
@@ -3795,6 +3847,7 @@ bool model_volume_list_changed(const ModelObject &model_object_old, const ModelO
});
}
template< typename TypeFilterFn, typename CompareFn>
bool model_property_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, TypeFilterFn type_filter, CompareFn compare)
{
+23 -1
View File
@@ -349,8 +349,22 @@ enum class ModelVolumeType : int {
PARAMETER_MODIFIER,
SUPPORT_BLOCKER,
SUPPORT_ENFORCER,
// Precise seam modifiers (6 subtypes for seam placement control).
// Order is critical: strong types first, then weak. Range checks in is_precise_seam*() depend on it.
PRECISE_SEAM_CENTER,
PRECISE_SEAM_LEFT,
PRECISE_SEAM_RIGHT,
PRECISE_SEAM_ENFORCED,
PRECISE_SEAM_BLOCKED,
PRECISE_SEAM_NEUTRAL,
};
// Free functions for checking ModelVolumeType without a ModelVolume object.
// Keep in sync with ModelVolume::is_precise_seam*() methods below.
inline bool is_precise_seam(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
inline bool is_precise_seam_strong(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
inline bool is_precise_seam_weak(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_ENFORCED && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials),
// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials.
// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed,
@@ -982,6 +996,14 @@ public:
bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; }
bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; }
bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; }
// Check if this volume is any of the precise seam modifier subtypes
bool is_precise_seam() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
// Helper to check if volume is a "strong" Precise Seam type (center, left, right)
// Strong modifiers have priority and always appear above weak modifiers in UI
bool is_precise_seam_strong() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
// Helper to check if volume is a "weak" Precise Seam type (enforced, blocked, neutral)
// Weak modifiers always appear below strong modifiers in UI
bool is_precise_seam_weak() const { return m_type >= ModelVolumeType::PRECISE_SEAM_ENFORCED && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
bool is_text() const { return text_configuration.has_value(); }
bool is_svg() const { return emboss_shape.has_value() && !text_configuration.has_value(); }
bool is_the_only_one_part() const; // behave like an object
@@ -1111,7 +1133,7 @@ protected:
friend class SLAPrint;
friend class Model;
friend class ModelObject;
friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new);
friend void model_volume_list_update_supports_and_seams(ModelObject& model_object_dst, const ModelObject& model_object_new);
// Copies IDs of both the ModelVolume and its config.
explicit ModelVolume(const ModelVolume &rhs) = default;
+37 -12
View File
@@ -2258,13 +2258,45 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
// ORCA:
// Inner Outer Inner wall ordering mode perimeter order optimisation functions
// Whether two Arachne lines touch: somewhere the gap between their centrelines is no more than the
// touching distance there. Each junction of one line is measured against the segments of the other,
// both ways, and the search stops at the first spot that touches.
// Arachne varies line width to fill the region (e.g. the odd centre line of a narrow wall is wider
// than nominal), so the touching distance is half the combined width at the closest points, not the
// nominal spacing. Widths are taken locally so a line widened in one place (a wedge tip, a wall
// transition) does not count as touching where it passes close to other perimeters. min_threshold keeps
// the nominal spacing threshold as the lower bound.
static bool arachne_lines_touch(const Arachne::ExtrusionLine &a, const Arachne::ExtrusionLine &b, double min_threshold)
{
auto one_way = [min_threshold](const Arachne::ExtrusionLine &from, const Arachne::ExtrusionLine &to) {
for (const Arachne::ExtrusionJunction &j : from.junctions) {
const Vec2d p = j.p.cast<double>();
for (size_t k = 0; k + 1 < to.junctions.size(); ++k) {
const Arachne::ExtrusionJunction &j0 = to.junctions[k];
const Arachne::ExtrusionJunction &j1 = to.junctions[k + 1];
const Vec2d s0 = j0.p.cast<double>();
const Vec2d seg = j1.p.cast<double>() - s0;
const double l2 = seg.squaredNorm();
const double t = l2 > 0. ? std::clamp((p - s0).dot(seg) / l2, 0., 1.) : 0.;
const double w = double(j0.w) + t * double(j1.w - j0.w); // width of `to` at the closest point
const double touch_distance = std::max(min_threshold, 0.5 * (double(j.w) + w));
if ((s0 + t * seg - p).norm() <= touch_distance)
return true;
}
}
return false;
};
return one_way(a, b) || one_way(b, a);
}
/**
* @brief Finds all perimeters touching a given set of reference lines, given as indexes.
*
* @param entities The list of PerimeterGeneratorArachneExtrusion entities.
* @param referenceIndices A set of indices representing the reference points.
* @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0
* @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+
* @param threshold_external The minimum touching distance for a reference perimeter with inset index 0
* @param threshold_internal The minimum touching distance for a reference perimeter with inset index 1+
* @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc).
* @return std::vector<int> A vector of indices representing the touching perimeters.
*/
@@ -2273,7 +2305,6 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
for (const int refIdx : referenceIndices) {
const auto& referenceEntity = entities[refIdx];
Points referencePoints = Arachne::to_points(*referenceEntity.extrusion);
for (size_t i = 0; i < entities.size(); ++i) {
// Skip already considered references and the reference entity
if (referenceIndices.count(i) > 0) continue;
@@ -2284,15 +2315,9 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
continue; // skip if they dont match
}
Points points = Arachne::to_points(*entity.extrusion);
double distance = MultiPoint::minimumDistanceBetweenLinesDefinedByPoints(referencePoints, points);
// Add to touchingIndices if within threshold distance
size_t threshold=0;
if(referenceEntity.extrusion->inset_idx == 0)
threshold = threshold_external;
else
threshold = threshold_internal;
if (distance <= threshold) {
// Add to touchingIndices if the lines touch.
const double threshold = double(referenceEntity.extrusion->inset_idx == 0 ? threshold_external : threshold_internal);
if (arachne_lines_touch(*referenceEntity.extrusion, *entity.extrusion, threshold)) {
touchingIndices.insert(i);
}
}
+3
View File
@@ -2,6 +2,7 @@
#define slic3r_PerimeterGenerator_hpp_
#include "libslic3r.h"
#include <optional>
#include <vector>
#include "Layer.hpp"
#include "Flow.hpp"
@@ -105,6 +106,8 @@ public:
bool has_fuzzy_hole = false;
// Preserve construction order so overlap precedence remains deterministic.
std::vector<std::pair<FuzzySkinConfig, ExPolygons>> regions_by_fuzzify;
// Area resting on the layer below, where fuzzy skin is allowed. Unset means no restriction.
std::optional<ExPolygons> fuzzy_supported_area;
PerimeterGenerator(
// Input:
+34 -11
View File
@@ -851,6 +851,8 @@ namespace client
// If true, the macro processor will evaluate just a boolean condition using the full expressive power of the macro processor.
bool just_boolean_expression = false;
std::string error_message;
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
mutable std::set<std::string> inactive_local_variables;
// Table to translate symbol tag to a human readable error message.
static std::map<std::string, std::string> tag_to_error_message;
@@ -892,6 +894,8 @@ namespace client
}
// Inside a block, which is conditionally suppressed?
bool skipping() const { return m_depth_suppressed > 0; }
// Are variable names resolved inside the suppressed blocks too?
bool check_inactive_names() const { return PlaceholderParser::check_inactive_branches && ! just_boolean_expression; }
const ConfigOption* optptr(const t_config_option_key &opt_key) const override
{
@@ -927,7 +931,7 @@ namespace client
static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output)
{
if (ctx->skipping())
if (ctx->skipping() && ! ctx->check_inactive_names())
return;
std::string opt_key_str(opt_key.begin(), opt_key.end());
@@ -949,7 +953,9 @@ namespace client
}
}
if (opt == nullptr)
ctx->throw_exception("Variable does not exist", opt_key);
ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key);
if (ctx->skipping())
return;
if (opt->is_scalar()) {
if (opt->is_nil())
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
@@ -972,9 +978,10 @@ namespace client
IteratorRange &opt_vector_index,
std::string &output)
{
if (ctx->skipping())
if (ctx->skipping() && ! ctx->check_inactive_names())
return;
const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist";
std::string opt_key_str(opt_key.begin(), opt_key.end());
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
if (opt == nullptr) {
@@ -984,18 +991,20 @@ namespace client
opt = ctx->resolve_symbol(opt_key_str);
}
if (opt == nullptr)
ctx->throw_exception("Variable does not exist", opt_key);
ctx->throw_exception(not_found, opt_key);
}
if (! opt->is_vector())
ctx->throw_exception("Trying to index a scalar variable", opt_key);
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
if (opt_index == nullptr)
ctx->throw_exception(not_found, opt_key);
if (opt_index->type() != coInt)
ctx->throw_exception("Indexing variable has to be integer", opt_key);
if (ctx->skipping())
return;
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt_key);
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
if (opt_index == nullptr)
ctx->throw_exception("Variable does not exist", opt_key);
if (opt_index->type() != coInt)
ctx->throw_exception("Indexing variable has to be integer", opt_key);
int idx = opt_index->getInt();
if (idx < 0)
ctx->throw_exception("Negative vector index", opt_key);
@@ -1021,6 +1030,13 @@ namespace client
output.writable = true;
}
output.opt = opt;
} else if (ctx->check_inactive_names()) {
// Only check the name. Back tracking may resolve the same identifier twice, so there are no side effects.
const std::string key{ opt_key.begin(), opt_key.end() };
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
ctx->inactive_local_variables.count(key) == 0 &&
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key);
}
output.it_range = opt_key;
}
@@ -1426,6 +1442,13 @@ namespace client
out.opt = ctx->config_local.optptr(key);
}
out.name = std::move(key);
} else if (ctx->check_inactive_names()) {
// Declared in a branch that is not taken: the name still counts as defined for the names that follow.
std::string key(it_range.begin(), it_range.end());
if (global_variable && ctx->context_data != nullptr)
ctx->context_data->inactive_global_variables.insert(std::move(key));
else
ctx->inactive_local_variables.insert(std::move(key));
}
out.it_range = it_range;
}
@@ -1660,8 +1683,8 @@ namespace client
const OptWithPos &rhs)
{
if (ctx->skipping())
// Skipping, continue parsing.
return true;
// Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference.
return false;
if (lhs.opt) {
assert(lhs.opt->is_vector());
+8
View File
@@ -4,6 +4,7 @@
#include "libslic3r.h"
#include <map>
#include <random>
#include <set>
#include <string>
#include <string_view>
#include <vector>
@@ -24,8 +25,15 @@ public:
// If defined, then this dictionary is used by the scripts to define user variables and persist them
// between PlaceholderParser evaluations.
std::unique_ptr<DynamicConfig> global_config;
// Global variables declared in {if} branches that were not taken, see check_inactive_branches.
std::set<std::string> inactive_global_variables;
};
// Orca: when set, variable names inside {if} branches that are not taken must resolve too, so a single
// expansion checks every branch of a template. Only the profile validator's slice sweep sets it.
// It does not apply to evaluate_boolean_expression(), where an error reads as "compatible".
static inline bool check_inactive_branches = false;
PlaceholderParser(const DynamicConfig *external_config = nullptr);
void clear_config() { m_config.clear(); }
+10 -1
View File
@@ -318,10 +318,13 @@ Points Polygon::concave_points(double angle_threshold) const
}
// Projection of a point onto the polygon.
Point Polygon::point_projection(const Point &point) const
Point Polygon::point_projection(const Point &point, size_t *edge_index) const
{
Point proj = point;
double dmin = std::numeric_limits<double>::max();
// Preserve the existing projection and tie order while optionally tracking its edge.
if (edge_index)
*edge_index = std::numeric_limits<size_t>::max();
if (! this->points.empty()) {
for (size_t i = 0; i < this->points.size(); ++ i) {
const Point &pt0 = this->points[i];
@@ -330,11 +333,15 @@ Point Polygon::point_projection(const Point &point) const
if (d < dmin) {
dmin = d;
proj = pt0;
if (edge_index)
*edge_index = i;
}
d = (point - pt1).cast<double>().norm();
if (d < dmin) {
dmin = d;
proj = pt1;
if (edge_index)
*edge_index = (i + 1) % this->points.size();
}
Vec2d v1(coordf_t(pt1(0) - pt0(0)), coordf_t(pt1(1) - pt0(1)));
coordf_t div = v1.squaredNorm();
@@ -347,6 +354,8 @@ Point Polygon::point_projection(const Point &point) const
if (d < dmin) {
dmin = d;
proj = foot;
if (edge_index)
*edge_index = i;
}
}
}
+3 -1
View File
@@ -85,7 +85,9 @@ public:
Points convex_points(double angle_threshold = 0.) const;
Points concave_points(double angle_threshold = 0.) const;
// Projection of a point onto the polygon.
Point point_projection(const Point &point) const;
// Optional index: start of the closest edge, or the vertex itself for an endpoint.
// Empty polygons return the query point and std::numeric_limits<size_t>::max() as the index.
Point point_projection(const Point &point, size_t *edge_index = nullptr) const;
std::vector<float> parameter_by_length() const;
//BBS
+11
View File
@@ -47,6 +47,7 @@
#include <boost/log/trivial.hpp>
#include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "Time.hpp"
#include "PlaceholderParser.hpp"
@@ -1198,6 +1199,7 @@ static std::vector<std::string> s_Preset_print_options{
"enable_tower_interface_features",
"enable_tower_interface_cooldown_during_tower",
"wipe_tower_no_sparse_layers",
"wipe_tower_sparse_layers_combination",
"compatible_printers",
"compatible_printers_condition",
"inherits",
@@ -2972,6 +2974,7 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
// 1) Find the preset with a new_name or create a new one,
// initialize it with the edited config.
auto it = this->find_preset_internal(new_name);
const bool preset_existed = (it != m_presets.end() && it->name == new_name);
if (it != m_presets.end() && it->name == new_name) {
// Preset with the same name found.
Preset &preset = *it;
@@ -3079,6 +3082,14 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
this->get_selected_preset().save(&(parent_preset->config));
else
this->get_selected_preset().save(nullptr);
{
LifecycleEventContext ctx;
ctx.name = new_name;
ctx.msg = preset_existed ? "overwrite" : "new";
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::PresetSaved, ctx);
}
}
// A detached standalone preset for the Full Publish receiver: create a user preset holding
+1
View File
@@ -62,6 +62,7 @@
#define ORCA_JSON_KEY_UPDATE_TIME "updated_time"
#define ORCA_JSON_KEY_CREATED_TIME "created_time"
#define BBL_JSON_KEY_INHERITS "inherits"
#define BBL_JSON_KEY_INCLUDES "include"
#define BBL_JSON_KEY_INSTANTIATION "instantiation"
#define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter"
#define BBL_JSON_KEY_PRINTER_TECH "machine_tech"
+73 -24
View File
@@ -199,7 +199,8 @@ DynamicPrintConfig PresetBundle::construct_full_config(
if (num_filaments <= 1) {
// BBS: update filament config related with variants
DynamicPrintConfig filament_config = in_filament_presets[0].config;
if (apply_extruder && ((extruder_count > 1) || different_extruder))
// Orca: a multi-variant filament resolves its variants on a single-variant printer too.
if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_config.has_multi_variant_filament()))
filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]);
out.apply(filament_config);
compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition());
@@ -223,7 +224,8 @@ DynamicPrintConfig PresetBundle::construct_full_config(
filament_temp_configs.resize(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) {
filament_temp_configs[i] = *(filament_configs[i]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
// Orca: a multi-variant filament resolves its variants on a single-variant printer too.
if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_temp_configs[i].has_multi_variant_filament()))
filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]);
}
@@ -4664,7 +4666,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
if (num_filaments <= 1) {
//BBS: update filament config related with variants
DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config;
if (apply_extruder && ((extruder_count > 1) || different_extruder))
// Orca: a multi-variant filament resolves its variants on a single-variant printer too.
if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_config.has_multi_variant_filament()))
filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]);
out.apply(filament_config);
compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition());
@@ -4758,7 +4761,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
filament_temp_configs.resize(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) {
filament_temp_configs[i] = *(filament_configs[i]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
// Orca: a multi-variant filament resolves its variants on a single-variant printer too.
if (apply_extruder && ((extruder_count > 1) || different_extruder || filament_temp_configs[i].has_multi_variant_filament()))
filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]);
}
@@ -6598,7 +6602,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// now, or deserialized from the vendor's cache; the code is shared so a
// cache-loaded bundle cannot come out different from a JSON-loaded one.
// Resolves `inherits` against the presets loaded before this one
// (config_maps) or against base_bundle's filament library, flattens, validates
// (config_maps) or against base_bundle's filament library, layers each
// `include` (include_maps) under the preset's own keys, flattens, validates
// and registers the preset. Returns the reason loading failed, empty on
// success.
std::string PresetBundle::load_vendor_preset(
@@ -6607,9 +6612,10 @@ std::string PresetBundle::load_vendor_preset(
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs)
const std::set<std::string>* retain_configs, const std::set<std::string>* retain_includes)
{
const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name);
const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path;
@@ -6652,14 +6658,30 @@ std::string PresetBundle::load_vendor_preset(
return reason;
}
}
else {
if (presets_collection->type() == Preset::TYPE_PRINTER)
default_config = &presets_collection->default_preset_for(entry.config_src).config;
else
default_config = &presets_collection->default_preset().config;
}
else
default_config = &presets_collection->default_preset_for(entry.config_src).config;
config = *default_config;
// Layer each included preset's own keys over the parent, in the order listed;
// this preset's own keys go on top.
for (const std::string& name : entry.includes) {
auto it = include_maps.find(name);
if (it == include_maps.end()) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find include " << name << " for " << preset_name;
continue;
}
config.apply(it->second);
}
config.apply(entry.config_src);
// Record what a base states, its diff against the default, for the presets
// that include it. It is taken before extend_default_config_length pads every
// per-variant key to the base's variant count: the padded defaults would
// otherwise override the values each includer inherits.
if (entry.instantiation == "false" && (retain_includes == nullptr || retain_includes->count(preset_name) != 0)) {
DynamicPrintConfig included;
included.apply_only(config, config.diff(presets_collection->default_preset_for(config).config));
include_maps.emplace(preset_name, std::move(included));
}
extend_default_config_length(config, true, *default_config);
if (entry.instantiation == "false" && "Template" != vendor_name) {
// Report configuration fields, which are misplaced into a wrong group.
@@ -7118,6 +7140,15 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
}
entry.name = key_values[BBL_JSON_KEY_NAME];
entry.description = key_values[BBL_JSON_KEY_DESCRIPTION];
// A file that states no instantiation and is named as G-code, or has no
// name, is a template that is only there to be included. A nameless one
// goes by its name in the vendor index.
if (auto it = key_values.find(BBL_JSON_KEY_INSTANTIATION);
(it == key_values.end() || it->second.empty()) && (entry.name.empty() || entry.name.find("gcode") != std::string::npos)) {
key_values[BBL_JSON_KEY_INSTANTIATION] = "false";
if (entry.name.empty())
entry.name = subfile_iter.first;
}
if(key_values.find(BBL_JSON_KEY_INSTANTIATION) == key_values.end())
{
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name;
@@ -7146,6 +7177,20 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
return reason;
}
}
if (auto it = key_values.find(BBL_JSON_KEY_INCLUDES); it != key_values.end()) {
// An array of names, or one bare name; load_from_json kept the JSON text.
nlohmann::json includes = nlohmann::json::parse(it->second);
if (!includes.is_array())
includes = nlohmann::json::array({std::move(includes)});
for (const auto& name : includes) {
if (name.is_string())
entry.includes.push_back(name.get<std::string>());
else {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid include " << name.dump() << " for " << entry.name;
}
}
}
if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) {
if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) {
BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << dir << "\": The preset \"" << entry.name
@@ -7162,7 +7207,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
return reason;
};
std::map<std::string, DynamicPrintConfig> configs;
std::map<std::string, DynamicPrintConfig> configs, include_maps;
std::map<std::string, std::string> filament_id_maps;
// Orca: whether to (re)write the vendor's cache after this parse, leaving it
// in step with the profile so the next run reads it instead. It is written
@@ -7179,6 +7224,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
auto load_subfiles = [&](std::vector<std::pair<std::string, std::string>>& subfiles,
std::vector<CachedPreset>& entries, const char* kind, bool is_from_lib = false) {
configs.clear();
include_maps.clear();
filament_id_maps.clear();
for (auto& subfile : subfiles) {
CachedPreset entry;
@@ -7186,8 +7232,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
if (reason.empty()) {
const int errors_before_install = m_errors;
reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags,
substitution_context, substitutions, configs, filament_id_maps, presets,
presets_loaded, is_from_lib);
substitution_context, substitutions, configs, include_maps, filament_id_maps,
presets, presets_loaded, is_from_lib);
install_errors += m_errors - errors_before_install;
}
if (!reason.empty()) {
@@ -7982,26 +8028,29 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s
// parsed), so no substitutions are reported, as before.
ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent };
PresetsConfigSubstitutions substitutions;
std::map<std::string, DynamicPrintConfig> configs;
std::map<std::string, DynamicPrintConfig> configs, include_maps;
std::map<std::string, std::string> filament_id_maps;
const std::string path = boost::filesystem::path(cache_path).parent_path().string();
size_t count = 0;
auto install_entries = [&](const std::vector<CachedPreset>& entries, PresetCollection* presets, bool is_from_lib) {
configs.clear();
include_maps.clear();
filament_id_maps.clear();
// Only configs of presets that other entries inherit are ever looked
// up again; registering just those skips one full config copy for
// every leaf preset. The library's filaments are all retained — they
// become the m_config_maps other vendors resolve against.
std::set<std::string> inherited;
for (const CachedPreset& entry : entries)
// Only configs of presets that other entries inherit or include are
// ever looked up again; registering just those skips one full config
// copy for every leaf preset. The library's filaments are all retained
// — they become the m_config_maps other vendors resolve against.
std::set<std::string> inherited, included;
for (const CachedPreset& entry : entries) {
if (! entry.inherits.empty())
inherited.insert(entry.inherits);
included.insert(entry.includes.begin(), entry.includes.end());
}
const std::set<std::string>* retain_configs = is_from_lib ? nullptr : &inherited;
for (const CachedPreset& entry : entries) {
const std::string reason = load_vendor_preset(entry, path, vendor_name,
base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions,
configs, filament_id_maps, presets, count, is_from_lib, retain_configs);
configs, include_maps, filament_id_maps, presets, count, is_from_lib, retain_configs, &included);
if (! reason.empty())
throw std::runtime_error("entry " + entry.name + " failed to install: " + reason);
}
+12 -10
View File
@@ -634,22 +634,24 @@ private:
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Load one source-form preset entry into this bundle: resolve `inherits`,
// flatten, validate and register the preset. Returns the reason loading
// failed, empty on success. See the definition for the sharing contract
// between the JSON parse and the cache load.
// retain_configs, when non-null, names the only presets registered into
// config_maps (a full config copy each). The cache load passes the names its
// entries inherit — the only ones ever looked up again; the JSON parse
// retains all, not knowing what later subfiles inherit.
// Load one source-form preset entry into this bundle: resolve `inherits`
// and `include`, flatten, validate and register the preset. Returns the
// reason loading failed, empty on success. See the definition for the
// sharing contract between the JSON parse and the cache load.
// retain_configs / retain_includes, when non-null, name the only presets
// registered into config_maps / include_maps (a config copy each). The
// cache load passes the names its entries inherit / include — the only
// ones ever looked up again; the JSON parse retains all, not knowing what
// later subfiles name.
std::string load_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr);
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
+2 -2
View File
@@ -254,7 +254,7 @@ constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
// save_entries below), or a change to the cache's own layout or the
// meaning of its stamps. Option-schema drift is NOT such a change — the
// dictionary handles it, which is why this no longer moves every release.
constexpr uint32_t CACHE_VERSION = 1;
constexpr uint32_t CACHE_VERSION = 2;
// A stamp-string read that refuses an absurd length before allocating anything.
// The stamps are read from files named from the outside (peek_version is
@@ -325,7 +325,7 @@ void visit_entry(Archive& ar, Entry& e, ConfigFn&& config)
{
ar(e.name, e.sub_path);
config();
ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
ar(e.inherits, e.includes, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
}
// The count comes from a file that has already passed magic and CRC, but a
+8 -6
View File
@@ -107,12 +107,13 @@ void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, con
// comes after.
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
// One preset as its JSON subfile states it: the config diff, the name of the
// preset it inherits, and the parse metadata — everything the parse phase of
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
// is resolved when the entry is installed, against whatever filament library
// is loaded then, so a cache carries no other vendor's values and no other
// vendor's update can make it stale.
// One preset as its JSON subfile states it: the config diff, the names of the
// preset it inherits and the presets it includes, and the parse metadata —
// everything the parse phase of load_vendor_configs_from_json extracts and
// nothing it derives. Inheritance and includes are resolved when the entry is
// installed, against whatever filament library is loaded then, so a cache
// carries no other vendor's values and no other vendor's update can make it
// stale.
// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every
// field below in this order — once, for the save, the load and the name peek alike.
struct CachedPreset
@@ -121,6 +122,7 @@ struct CachedPreset
std::string sub_path; // path under the vendor's directory
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
std::string inherits;
std::vector<std::string> includes; // layered under config_src, in this order
std::string description;
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
std::string setting_id;
+76 -4
View File
@@ -14,6 +14,7 @@
#include "Flow.hpp"
#include "Geometry/ConvexHull.hpp"
#include "I18N.hpp"
#include "LifecycleEvents.hpp"
#include "ShortestPath.hpp"
#include "Thread.hpp"
#include "Time.hpp"
@@ -377,6 +378,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_bridging"
|| opt_key == "wipe_tower_extra_flow"
|| opt_key == "wipe_tower_no_sparse_layers"
|| opt_key == "wipe_tower_sparse_layers_combination"
|| opt_key == "flush_volumes_matrix"
|| opt_key == "prime_volume"
|| opt_key == "flush_into_infill"
@@ -1704,6 +1706,25 @@ StringObjectException Print::check_multi_filament_valid(const Print& print)
// Precondition: Print::validate() requires the Print::apply() to be called its invocation.
//BBS: refine seq-print validation logic
// The exception's own message is just "Errors"; the detail is in the per-object errors,
// whose object id is the PrintObject's.
std::string Print::slicing_errors_message(const SlicingErrors &errors) const
{
std::string message;
for (const SlicingError &error : errors.errors_) {
std::string object_name;
for (const PrintObject *object : m_objects)
if (object->id().id == error.objectId()) {
object_name = object->model_object()->name;
break;
}
if (!message.empty())
message += "\n";
message += object_name.empty() ? std::string(error.what()) : object_name + ": " + error.what();
}
return message;
}
StringObjectException Print::validate(std::vector<StringObjectException> *warnings, Polygons* collison_polygons, std::vector<std::pair<Polygon, float>>* height_polygons) const
{
auto add_warning = [warnings](StringObjectException w) {
@@ -2694,6 +2715,15 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
}
for (PrintObject *obj : m_objects)
obj->clear_shared_object();
@@ -3312,6 +3342,15 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
BOOST_LOG_TRIVIAL(info) << "Slicing process finished." << log_memory_info();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
}
}
// G-code export process, running at a background thread.
@@ -3899,6 +3938,9 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
{
int extruder_count = 1, extruder_volume_type_count = 1;
bool support_multi = m_ori_full_print_config.support_different_extruders(extruder_count);
// Orca: resolve the filament variants wherever Print::apply does, a multi-variant filament
// on a single-variant printer included.
const bool expand_filaments = (extruder_count > 1) || support_multi || m_ori_full_print_config.has_multi_variant_filament();
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
@@ -3939,7 +3981,7 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
m_full_print_config = m_ori_full_print_config;
std::set<std::string> filament_keys = filament_options_with_variant;
filament_keys.insert("filament_self_index");
if ((extruder_count > 1) || support_multi)
if (expand_filaments)
m_full_print_config.update_values_to_printer_extruders_for_multiple_filaments(m_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant");
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
@@ -3956,7 +3998,7 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, m_config.filament_map_2.values);
}
if ((extruder_count > 1) || support_multi) {
if (expand_filaments) {
t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end());
keys.push_back("filament_self_index");
m_config.apply_only(m_full_print_config, keys, true);
@@ -4911,6 +4953,16 @@ void Print::set_gcode_file_invalidated()
//BBS: add gcode file preload logic
void Print::export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb)
{
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportStarted, ctx);
}
try {
GCodeProcessor processor;
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
@@ -4930,13 +4982,32 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
*result = std::move(processor.extract_result());
result->filament_change_sequence = filament_seq_loaded;
result->nozzle_change_sequence = nozzle_seq_loaded;
} catch (std::exception & /* ex */) {
} catch (std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
}
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
@@ -4981,6 +5052,7 @@ DynamicConfig PrintStatistics::config() const
config.set_key_value("total_wipe_tower_filament", new ConfigOptionFloat(this->total_wipe_tower_filament));
config.set_key_value("initial_tool", new ConfigOptionInt(static_cast<int>(this->initial_tool)));
config.set_key_value("initial_extruder", new ConfigOptionInt(static_cast<int>(this->initial_tool)));
config.set_key_value("initial_no_support_extruder", new ConfigOptionInt(static_cast<int>(this->initial_no_support_tool)));
return config;
}
@@ -4990,7 +5062,7 @@ DynamicConfig PrintStatistics::placeholders()
for (const std::string key : {
"print_time", "normal_print_time", "silent_print_time",
"used_filament", "extruded_volume", "extruded_volume_total", "total_cost", "total_weight", "extruded_weight_total",
"initial_tool", "initial_extruder", "total_toolchanges", "total_wipe_tower_cost", "total_wipe_tower_filament"})
"initial_tool", "initial_extruder", "initial_no_support_extruder", "total_toolchanges", "total_wipe_tower_cost", "total_wipe_tower_filament"})
config.set_key_value(key, new ConfigOptionString(std::string("{") + key + "}"));
return config;
}
+10
View File
@@ -30,6 +30,8 @@
namespace Slic3r {
class SlicingErrors;
class GCode;
class Layer;
class ModelObject;
@@ -468,6 +470,10 @@ public:
std::vector<Polygons> slice_support_volumes(const ModelVolumeType model_volume_type) const;
std::vector<Polygons> slice_support_blockers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_BLOCKER); }
std::vector<Polygons> slice_support_enforcers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_ENFORCER); }
// Shared slicing path; multiple volumes are united per layer.
std::vector<Polygons> slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const;
// Keep Precise Seam volumes separate so their individual priority is preserved.
std::vector<Polygons> slice_single_volume(const ModelVolume* volume) const { return this->slice_modifier_volumes({volume}); }
// Helpers to project custom facets on slices
void project_and_append_custom_facets(bool seam, EnforcerBlockerType type, std::vector<Polygons>& expolys, std::vector<std::pair<Vec3f,Vec3f>>* vertical_points=nullptr) const;
@@ -828,6 +834,7 @@ struct PrintStatistics
double total_wipe_tower_cost;
double total_wipe_tower_filament;
unsigned int initial_tool;
unsigned int initial_no_support_tool;
std::map<size_t, double> filament_stats;
// Config with the filled in print statistics.
@@ -846,6 +853,7 @@ struct PrintStatistics
total_wipe_tower_cost = 0.;
total_wipe_tower_filament = 0.;
initial_tool = 0;
initial_no_support_tool = 0;
filament_stats.clear();
}
static const std::string FilamentUsedG;
@@ -967,6 +975,8 @@ public:
// Returns an empty string if valid, otherwise returns an error message.
StringObjectException validate(std::vector<StringObjectException> *warnings = nullptr, Polygons* collison_polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr) const override;
// The per-object messages of a SlicingErrors, each prefixed with its object's name.
std::string slicing_errors_message(const SlicingErrors &errors) const;
double skirt_first_layer_height() const;
Flow brim_flow() const;
Flow skirt_flow() const;
+54 -16
View File
@@ -8,12 +8,12 @@
namespace Slic3r {
// Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new
// Add or remove support and Precise Seam modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new
// in the exact order and with the same IDs.
// It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order.
// Other volume types must already match model_object_new in the correct order.
// Friend to ModelVolume to allow copying.
// static is not accepted by gcc if declared as a friend of ModelObject.
/* static */ void model_volume_list_update_supports(ModelObject &model_object_dst, const ModelObject &model_object_new)
/* static */ void model_volume_list_update_supports_and_seams(ModelObject &model_object_dst, const ModelObject &model_object_new)
{
typedef std::pair<const ModelVolume*, bool> ModelVolumeWithStatus;
std::vector<ModelVolumeWithStatus> old_volumes;
@@ -33,18 +33,22 @@ namespace Slic3r {
assert(! it->second); // not consumed yet
it->second = true;
ModelVolume *model_volume_dst = const_cast<ModelVolume*>(it->first);
// For support modifiers, the type may have been switched from blocker to enforcer and vice versa.
assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) || model_volume_dst->type() == model_volume_src->type());
// Type may switch within support family, within precise_seam family, or between them.
assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) ||
(model_volume_dst->is_precise_seam() && model_volume_src->is_precise_seam()) ||
(model_volume_dst->is_support_modifier() && model_volume_src->is_precise_seam()) ||
(model_volume_dst->is_precise_seam() && model_volume_src->is_support_modifier()) ||
model_volume_dst->type() == model_volume_src->type());
model_object_dst.volumes.emplace_back(model_volume_dst);
if (model_volume_dst->is_support_modifier()) {
// For support modifiers, the type may have been switched from blocker to enforcer and vice versa.
if (model_volume_dst->is_support_modifier() || model_volume_dst->is_precise_seam()) {
// Type may have been switched within or between support/precise_seam families.
model_volume_dst->set_type(model_volume_src->type());
model_volume_dst->set_transformation(model_volume_src->get_transformation());
}
assert(model_volume_dst->get_matrix().isApprox(model_volume_src->get_matrix()));
} else {
// The volume was not found in the old list. Create a new copy.
assert(model_volume_src->is_support_modifier());
assert(model_volume_src->is_support_modifier() || model_volume_src->is_precise_seam());
model_object_dst.volumes.emplace_back(new ModelVolume(*model_volume_src));
model_object_dst.volumes.back()->set_model_object(&model_object_dst);
}
@@ -55,17 +59,20 @@ namespace Slic3r {
delete mv_with_status.first;
}
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type)
// Copy configs of ModelVolumes matching type_filter predicate from src to dst.
// Mirrors the template pattern of model_volume_list_changed() in Model.cpp.
template<typename TypeFilterFn>
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, TypeFilterFn type_filter)
{
size_t i_src, i_dst;
for (i_src = 0, i_dst = 0; i_src < model_object_src.volumes.size() && i_dst < model_object_dst.volumes.size();) {
const ModelVolume &mv_src = *model_object_src.volumes[i_src];
ModelVolume &mv_dst = *model_object_dst.volumes[i_dst];
if (mv_src.type() != type) {
if (! type_filter(mv_src.type())) {
++ i_src;
continue;
}
if (mv_dst.type() != type) {
if (! type_filter(mv_dst.type())) {
++ i_dst;
continue;
}
@@ -88,6 +95,20 @@ static inline void model_volume_list_copy_configs(ModelObject &model_object_dst,
}
}
// Convenience overload: single volume type.
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type)
{
model_volume_list_copy_configs(model_object_dst, model_object_src, [type](const ModelVolumeType t) { return t == type; });
}
// Convenience overload: multiple volume types at once (e.g. all precise seam types).
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const std::initializer_list<ModelVolumeType> &types)
{
model_volume_list_copy_configs(model_object_dst, model_object_src, [&types](const ModelVolumeType t) {
return std::find(types.begin(), types.end(), t) != types.end();
});
}
static inline void layer_height_ranges_copy_configs(t_layer_config_ranges &lr_dst, const t_layer_config_ranges &lr_src)
{
assert(lr_dst.size() == lr_src.size());
@@ -1296,7 +1317,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
extruder_count, extruder_volume_type_count, filament_keys,
"filament_self_index", "filament_extruder_variant",
&dynamic_slot_indices);
else if ((extruder_count > 1) || different_extruder)
// Orca: also on a printer with a single extruder variant once a filament defines several
// (e.g. Standard and High Flow), so each filament takes its variant for that extruder.
else if ((extruder_count > 1) || different_extruder || new_full_config.has_multi_variant_filament())
new_full_config.update_values_to_printer_extruders_for_multiple_filaments(m_ori_full_print_config, extruder_count, extruder_volume_type_count, filament_keys,
"filament_self_index", "filament_extruder_variant");
}
@@ -1618,6 +1641,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// 3) Synchronize ModelObjects & PrintObjects.
const std::initializer_list<ModelVolumeType> solid_or_modifier_types { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME, ModelVolumeType::PARAMETER_MODIFIER };
const std::initializer_list<ModelVolumeType> precise_seam_types {
ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_LEFT,
ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_ENFORCED,
ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL
};
for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) {
ModelObject &model_object = *m_model.objects[idx_model_object];
ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(model_object));
@@ -1635,6 +1663,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
model_fuzzy_skin_data_changed(model_object, model_object_new);
bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) ||
model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_ENFORCER);
bool precise_seam_differ = model_volume_list_changed(model_object, model_object_new, precise_seam_types);
bool layer_height_ranges_differ = ! layer_height_ranges_equal(model_object.layer_config_ranges, model_object_new.layer_config_ranges, model_object_new.layer_height_profile.empty());
bool model_origin_translation_differ = model_object.origin_translation != model_object_new.origin_translation;
bool brim_points_differ = model_brim_points_data_changed(model_object, model_object_new);
@@ -1677,13 +1706,20 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Invalidate just the supports step.
for (const PrintObjectStatus &print_object_status : print_objects_range)
update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial));
if (supports_differ) {
// Copy just the support volumes.
model_volume_list_update_supports(model_object, model_object_new);
}
}
if (precise_seam_differ) {
// First stop background processing before shuffling or deleting the ModelVolumes in the ModelObject's list.
this->call_cancel_callback();
update_apply_status(false);
// Invalidate seam placement (affects G-code export).
update_apply_status(this->invalidate_step(psGCodeExport));
} else if (model_custom_seam_data_changed(model_object, model_object_new)) {
update_apply_status(this->invalidate_step(psGCodeExport));
}
// Synchronize both families once, after cancellation and all affected-step invalidations.
// This also handles type changes between supports and Precise Seam before copying configs below.
if (supports_differ || precise_seam_differ)
model_volume_list_update_supports_and_seams(model_object, model_object_new);
if (brim_points_differ) {
model_object.brim_points = model_object_new.brim_points;
update_apply_status(this->invalidate_all_steps());
@@ -1708,6 +1744,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
//FIXME What to do with m_material_id?
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::MODEL_PART);
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::PARAMETER_MODIFIER);
// Synchronize Precise Seam modifier volumes
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, precise_seam_types);
layer_height_ranges_copy_configs(model_object.layer_config_ranges /* dst */, model_object_new.layer_config_ranges /* src */);
// Copy the ModelObject name, input_file and instances. The instances will be compared against PrintObject instances in the next step.
model_object.name = model_object_new.name;
+2 -1
View File
@@ -62,7 +62,8 @@ public:
SlicingReplaceInitEmptyLayers,
SlicingNeedSupportOn,
SlicingEmptyGcodeLayers,
SlicingGcodeOverlap
SlicingGcodeOverlap,
SlicingPreciseSeamWarning
};
typedef size_t TimeStamp;
+85 -9
View File
@@ -626,7 +626,9 @@ static const t_config_enum_values s_keys_map_NozzleVolumeType = {
{ "Standard", nvtStandard },
{ "High Flow", nvtHighFlow },
{ "TPU High Flow", nvtTPUHighFlow },
{ "Hybrid", nvtHybrid }
{ "Hybrid", nvtHybrid },
{ "E3D High Flow", nvtE3DHighFlow },
{ "Extra High Flow", nvtExtraHighFlow }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleVolumeType)
@@ -676,9 +678,34 @@ int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_ty
for (int index = 0; index < int(variant_list.size()); ++index) {
if (extruder_variant == variant_list[index] && variant_ids_1based[index] == variant_id_1based) { return index; }
}
// Without this variant, use the id's own first variant (usually Standard), not variant index 0,
// which belongs to the first filament or extruder.
for (int index = 0; index < int(variant_list.size()); ++index) {
if (variant_ids_1based[index] == variant_id_1based) { return index; }
}
return 0;
}
std::set<NozzleVolumeType> get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id)
{
std::set<NozzleVolumeType> supported_types;
auto *variant_list = printer_config.option<ConfigOptionStrings>("extruder_variant_list");
auto *extruder_types = printer_config.option<ConfigOptionEnumsGeneric>("extruder_type");
if (!variant_list || !extruder_types || extruder_id < 0 ||
extruder_id >= (int) variant_list->values.size() || extruder_id >= (int) extruder_types->values.size())
return supported_types;
const ExtruderType extruder_type = ExtruderType(extruder_types->values[extruder_id]);
for (NozzleVolumeType volume_type : get_valid_nozzle_volume_type()) {
// An unsupported extruder type yields an empty name, which would match any list.
const std::string variant = get_extruder_variant_string(extruder_type, volume_type);
if (!variant.empty() && variant_list->values[extruder_id].find(variant) != std::string::npos)
supported_types.insert(volume_type);
}
return supported_types;
}
std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type)
{
if (nozzle_volume_type > nvtMaxNozzleVolumeType) {
@@ -1522,7 +1549,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(1));
def = this->add("top_solid_infill_flow_ratio", coFloat);
def = this->add("top_solid_infill_flow_ratio", coFloats);
def->label = L("Top surface flow ratio");
def->category = L("Advanced");
def->tooltip = L("This factor affects the amount of material for top solid infill. "
@@ -1531,7 +1558,8 @@ void PrintConfigDef::init_fff_params()
def->min = 0;
def->max = 2;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(1));
def->nullable = true;
def->set_default_value(new ConfigOptionFloatsNullable{1});
def = this->add("bottom_solid_infill_flow_ratio", coFloat);
def->label = L("Bottom surface flow ratio");
@@ -5922,15 +5950,20 @@ void PrintConfigDef::init_fff_params()
def->label = "Nozzle Volume Type";
def->tooltip = "Nozzle volume type for extruders.";
def->enum_keys_map = &ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
// Order must match the NozzleVolumeType enum values (Standard=0, High Flow=1, Hybrid=2, TPU High Flow=3).
// Listed in display order. A position is not the enum value (E3D High Flow is 5, after the reserved 4),
// so map a position to its NozzleVolumeType through enum_keys_map.
def->enum_values.push_back(L("Standard"));
def->enum_values.push_back(L("High Flow"));
def->enum_values.push_back(L("Hybrid"));
def->enum_values.push_back(L("TPU High Flow"));
def->enum_values.push_back(L("E3D High Flow"));
def->enum_values.push_back(L("Extra High Flow"));
def->enum_labels.push_back(L("Standard"));
def->enum_labels.push_back(L("High Flow"));
def->enum_labels.push_back(L("Hybrid"));
def->enum_labels.push_back(L("TPU High Flow"));
def->enum_labels.push_back(L("E3D High Flow"));
def->enum_labels.push_back(L("Extra High Flow"));
def->mode = comSimple;
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
@@ -5943,10 +5976,14 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back(L("High Flow"));
def->enum_values.push_back(L("Hybrid"));
def->enum_values.push_back(L("TPU High Flow"));
def->enum_values.push_back(L("E3D High Flow"));
def->enum_values.push_back(L("Extra High Flow"));
def->enum_labels.push_back(L("Standard"));
def->enum_labels.push_back(L("High Flow"));
def->enum_labels.push_back(L("Hybrid"));
def->enum_labels.push_back(L("TPU High Flow"));
def->enum_labels.push_back(L("E3D High Flow"));
def->enum_labels.push_back(L("Extra High Flow"));
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
@@ -5985,7 +6022,7 @@ void PrintConfigDef::init_fff_params()
// Per-nozzle volume type. Forward-compat-only registration with no slicing consumer — nothing in
// src/ reads it; the engine resolves per-nozzle volume types from `extruder_nozzle_stats` tokens
// instead. Kept registered so a project/config carrying it loads without an unknown-option
// substitution warning. Registers Standard/High Flow/TPU High Flow only (no Hybrid).
// substitution warning. Registers the physical types only (no Hybrid).
// Internal use only, no translation.
def = this->add("extruder_nozzle_volume_type", coEnums);
def->label = "Extruder nozzle volume type";
@@ -5994,9 +6031,13 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back("Standard");
def->enum_values.push_back("High Flow");
def->enum_values.push_back("TPU High Flow");
def->enum_values.push_back("E3D High Flow");
def->enum_values.push_back("Extra High Flow");
def->enum_labels.push_back("Standard");
def->enum_labels.push_back("High Flow");
def->enum_labels.push_back("TPU High Flow");
def->enum_labels.push_back("E3D High Flow");
def->enum_labels.push_back("Extra High Flow");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnumsGeneric{ NozzleVolumeType::nvtStandard });
@@ -6704,6 +6745,20 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("wipe_tower_sparse_layers_combination", coBool);
def->label = L("Combine sparse layers");
def->tooltip = L("If enabled, consecutive layers on which the prime tower has no filament change are printed as a single "
"thicker tower layer instead of one thin layer each, the same way infill combination merges sparse infill. "
"The merged layer is printed at the top of the run, at the height of everything it covers.\n\n"
"Only whole layers are merged, and never past the maximum layer height of the nozzle printing the tower "
"(three quarters of the nozzle diameter when that is left at 0). Two or more layers therefore have to fit "
"under that limit before anything changes at all: at a 0.2 mm layer height under a 0.3 mm maximum nothing "
"is merged, while at 0.1 mm three layers become one.\n\n"
"Unlike \"No sparse layers\" the tower keeps following the model, so the toolhead never has to reach down to it. "
"Has no effect with \"No sparse layers\", smooth timelapse or clumping detection, which need a tower on every layer.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("single_extruder_multi_material_priming", coBool);
def->label = L("Prime all printing extruders");
def->tooltip = L("If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print.");
@@ -9127,6 +9182,8 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
value = "tree(auto)";
} else if (opt_key == "support_base_pattern" && value == "none") {
value = "hollow";
} else if (opt_key == "tree_support_wall_count" && value == "-1") {
value = "0";
} else if (opt_key == "different_settings_to_system") {
std::string copy_value = value;
copy_value.erase(std::remove(copy_value.begin(), copy_value.end(), '\"'), copy_value.end()); // remove '"' in string
@@ -9395,7 +9452,8 @@ std::set<std::string> print_options_with_variant = {
"initial_layer_travel_jerk",
"default_junction_deviation",
"print_extruder_id", //coInts
"print_extruder_variant" //coStrings
"print_extruder_variant", //coStrings
"top_solid_infill_flow_ratio"
};
std::set<std::string> filament_options_with_variant = {
@@ -10052,6 +10110,13 @@ bool DynamicPrintConfig::is_using_different_extruders()
return ret;
}
bool DynamicPrintConfig::has_multi_variant_filament() const
{
auto variants = dynamic_cast<const ConfigOptionStrings*>(this->option("filament_extruder_variant"));
auto diameters = dynamic_cast<const ConfigOptionFloats*>(this->option("filament_diameter"));
return variants && diameters && variants->size() > diameters->size();
}
bool DynamicPrintConfig::support_different_extruders(int& extruder_count) const
{
std::set<std::string> variant_set;
@@ -11052,6 +11117,11 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
return;
}
std::vector<int> filament_maps = opt_filament_map->values;
auto opt_ids = id_name.empty()? nullptr: dynamic_cast<const ConfigOptionInts*>(this->option(id_name));
// Orca: a map shorter than the filament count must not drop the filaments past its end;
// they take the first extruder.
if (opt_ids && !opt_ids->values.empty())
filament_maps.resize(std::max<size_t>(filament_maps.size(), *std::max_element(opt_ids->values.begin(), opt_ids->values.end())), 1);
size_t filament_count = filament_maps.size();
//apply process settings
auto opt_extruder_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(printer_config.option("extruder_type"));
@@ -11070,7 +11140,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
// indexed out of bounds.
if (opt_filament_volume_maps && opt_filament_volume_maps->values.size() == filament_count)
filament_volume_maps = opt_filament_volume_maps->values;
auto opt_ids = id_name.empty()? nullptr: dynamic_cast<const ConfigOptionInts*>(this->option(id_name));
std::vector<int> variant_index;
variant_index.resize(filament_count, -1);
@@ -11087,9 +11156,10 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
//variant index
variant_index[f_index] = get_index_for_extruder(f_index+1, id_name, extruder_type, nozzle_volume_type, variant_name);
if (variant_index[f_index] < 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%")
// Orca: a filament need not define every extruder variant (a Direct Drive filament on a
// Bowden printer), so this is not an invalid state: the filament's first variant is used.
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: could not found extruder_type %2%, nozzle_volume_type %3%, filament_index %4%, extruder index %5%")
%__LINE__ %s_keys_names_ExtruderType[extruder_type] % s_keys_names_NozzleVolumeType[nozzle_volume_type] % (f_index+1) %filament_maps[f_index];
assert(false);
//for some updates happens in a invalid state(caused by popup window)
//we need to avoid crash
variant_index[f_index] = 0;
@@ -12347,6 +12417,12 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def->tooltip = L("If enabled, Arrange will allow rotation when placing objects.");
def->set_default_value(new ConfigOptionBool(true));
def = this->add("align_to_y_axis", coBool);
def->label = L("Align to Y axis when arranging");
def->tooltip = L("If enabled, Arrange will turn each object so its long side runs along the Y axis before placing it. "
"When not given, it is on for i3 printers and off for the others, as in the GUI.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("avoid_extrusion_cali_region", coBool);
def->label = L("Avoid extrusion calibrate region when arranging");
def->tooltip = L("If enabled, Arrange will avoid extrusion calibrate region when placing objects.");
+21 -4
View File
@@ -30,6 +30,8 @@
namespace Slic3r {
class DynamicPrintConfig;
enum GCodeFlavor : unsigned char {
gcfMarlinLegacy,
gcfKlipper,
@@ -163,6 +165,7 @@ inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline =
case ipGrid:
case ipTriangles:
case ipStars:
case ipCubic:
return multiline > 1;
default:
return false;
@@ -518,8 +521,11 @@ enum NozzleVolumeType {
// with more than one sub-nozzle (extruder_max_nozzle_count > 1); matched as Standard for
// preset lookup and never emitted in profile variant strings
nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only
// 4 is reserved: E3D High Flow is 5 in BambuStudio's slice_info and device numbering.
nvtE3DHighFlow = 5, // physical variant, E3D high-flow hotend on 0.4/0.6 nozzles
nvtExtraHighFlow = 6, // Orca: physical variant with no BambuStudio or device counterpart; only profiles name it
// Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable.
nvtMaxNozzleVolumeType = nvtTPUHighFlow
nvtMaxNozzleVolumeType = nvtExtraHighFlow
};
enum FilamentMapMode {
@@ -557,11 +563,19 @@ static std::set<NozzleVolumeType> get_valid_nozzle_volume_type() {
// Hybrid is not a physical nozzle variant: presets never define it, so it must not
// produce a variant string.
if (t == nvtHybrid) continue;
// Skip the reserved gap between nvtTPUHighFlow (3) and nvtE3DHighFlow (5).
if (i > nvtTPUHighFlow && i < nvtE3DHighFlow) continue;
type.insert(t);
}
return type;
}
// The nozzle volume types the given extruder physically provides, as declared by the printer
// profile's extruder_variant_list. An empty set means the profile could not be read and must be
// treated as "unknown", not as "none". nvtHybrid is never reported: it describes an extruder
// holding a mix of nozzles, not a nozzle the profile can offer.
extern std::set<NozzleVolumeType> get_extruder_supported_nozzle_volume_types(const DynamicPrintConfig &printer_config, int extruder_id);
std::string get_nozzle_volume_type_string(NozzleVolumeType nozzle_volume_type);
static std::string bed_type_to_gcode_string(const BedType type)
@@ -698,8 +712,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
class DynamicPrintConfig;
// Defines each and every configuration option of Slic3r, including the properties of the GUI dialogs.
// Does not store the actual values, but defines default values.
class PrintConfigDef : public ConfigDef
@@ -820,6 +832,10 @@ public:
//BBS
bool is_using_different_extruders();
bool support_different_extruders(int& extruder_count) const;
// Whether any filament defines more than one variant (filament_extruder_variant longer than
// filament_diameter). Its variants then have to be resolved even on a printer with a single
// extruder variant, which picks the filament's variant of the same variant string.
bool has_multi_variant_filament() const;
// Counts the config slots of a printer: one per (extruder x nozzle volume type) as described by
// extruder_nozzle_stats, or simply one per extruder when the stats are absent/mismatched.
// Fills nozzle_volume_types with each extruder's volume types in ascending enum order.
@@ -1403,7 +1419,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatsNullable, small_perimeter_threshold))
((ConfigOptionFloatsOrPercentsNullable, small_support_perimeter_speed))
((ConfigOptionFloatsNullable, small_support_perimeter_threshold))
((ConfigOptionFloat, top_solid_infill_flow_ratio))
((ConfigOptionFloatsNullable, top_solid_infill_flow_ratio))
((ConfigOptionFloat, bottom_solid_infill_flow_ratio))
((ConfigOptionFloatOrPercent, infill_anchor))
((ConfigOptionFloatOrPercent, infill_anchor_max))
@@ -1631,6 +1647,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionBool, wipe_tower_sparse_layers_combination))
((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode))
((ConfigOptionString, process_change_extrusion_role_gcode))
+29 -23
View File
@@ -1551,12 +1551,19 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
{
auto it_volume = this->model_object()->volumes.begin();
auto it_volume_end = this->model_object()->volumes.end();
for (; it_volume != it_volume_end && (*it_volume)->type() != model_volume_type; ++ it_volume) ;
// Supports merge every matching volume; Precise Seam calls the shared slicer one volume at a time.
std::vector<const ModelVolume*> volumes;
for (const ModelVolume *volume : this->model_object()->volumes)
if (volume->type() == model_volume_type)
volumes.push_back(volume);
return this->slice_modifier_volumes(volumes);
}
std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const
{
std::vector<Polygons> slices;
if (it_volume != it_volume_end) {
// Found at least a single support volume of model_volume_type.
if (!volumes.empty()) {
// Share layer heights, transforms and cancellation handling across the selected volumes.
std::vector<float> zs = zs_from_layers(this->layers());
std::vector<char> merge_layers;
bool merge = false;
@@ -1564,27 +1571,26 @@ std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType m
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
MeshSlicingParamsEx params;
params.trafo = this->trafo_centered();
for (; it_volume != it_volume_end; ++ it_volume)
if ((*it_volume)->type() == model_volume_type) {
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
if (slices.empty()) {
slices.reserve(slices2.size());
for (ExPolygons &src : slices2)
slices.emplace_back(to_polygons(std::move(src)));
} else if (!slices2.empty()) {
if (merge_layers.empty())
merge_layers.assign(zs.size(), false);
for (size_t i = 0; i < zs.size(); ++ i) {
if (slices[i].empty())
slices[i] = to_polygons(std::move(slices2[i]));
else if (! slices2[i].empty()) {
append(slices[i], to_polygons(std::move(slices2[i])));
merge_layers[i] = true;
merge = true;
}
for (const ModelVolume *volume : volumes) {
std::vector<ExPolygons> slices2 = slice_volume(*volume, zs, params, throw_on_cancel_callback);
if (slices.empty()) {
slices.reserve(slices2.size());
for (ExPolygons &src : slices2)
slices.emplace_back(to_polygons(std::move(src)));
} else if (!slices2.empty()) {
if (merge_layers.empty())
merge_layers.assign(zs.size(), false);
for (size_t i = 0; i < zs.size(); ++ i) {
if (slices[i].empty())
slices[i] = to_polygons(std::move(slices2[i]));
else if (! slices2[i].empty()) {
append(slices[i], to_polygons(std::move(slices2[i])));
merge_layers[i] = true;
merge = true;
}
}
}
}
if (merge) {
std::vector<Polygons*> to_merge;
to_merge.reserve(zs.size());
+68 -43
View File
@@ -1785,6 +1785,13 @@ 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,11 +1826,12 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
for (auto [triangle_id, ibit] : data.triangles_to_split) {
assert(triangle_id < int(m_triangles.size()));
assert(ibit < int(data.bitstream.size()));
auto next_nibble = [&data, &ibit = ibit]() {
// 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]() {
int n = 0;
for (int i = 0; i < 4; ++ i)
n |= data.bitstream[ibit ++] << i;
if (! data.read_nibble(ibit, n))
corrupt = true;
return n;
};
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
@@ -1842,6 +1850,10 @@ 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)
@@ -1856,7 +1868,7 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
}
// Only valid if is_split.
int special_side = code >> 2;
int special_side = num_of_split_sides == 3 ? 0 : code >> 2;
// Take care of the first iteration separately, so handling of the others is simpler.
if (parents.empty()) {
@@ -1911,47 +1923,55 @@ 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);
}
}
}
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);
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;
if (this->bitstream.empty() || this->bitstream.size() == bitstream_start_idx)
return;
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;
}
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;
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;
}
assert(facet_state < this->used_states.size());
if (facet_state >= this->used_states.size())
continue;
} while (static_cast<size_t>(ibit) < this->bitstream.size());
this->used_states[facet_state] = true;
}
// 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;
}
// Lightweight variant of deserialization, which only tests whether a face of test_state exists.
@@ -1963,11 +1983,12 @@ 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;
assert(ibit < int(data.bitstream.size()));
auto next_nibble = [&data, &ibit = ibit]() {
// Stop reading a triangle whose stream is truncated.
bool truncated = false;
auto next_nibble = [&data, &ibit = ibit, &truncated]() {
int n = 0;
for (int i = 0; i < 4; ++ i)
n |= data.bitstream[ibit ++] << i;
if (! data.read_nibble(ibit, n))
truncated = true;
return n;
};
// < 0 -> negative of a number of children
@@ -1985,6 +2006,8 @@ 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();
@@ -1992,6 +2015,8 @@ 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);
+14 -2
View File
@@ -297,8 +297,20 @@ public:
std::fill(used_states.begin(), used_states.end(), false);
}
// 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);
// 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;
}
private:
friend class cereal::access;
+15
View File
@@ -285,6 +285,21 @@ inline std::string sanitize_filename(const std::string &filename){
const std::regex special_chars("[/\\\\:*?\"<>|]");
return std::regex_replace(filename, special_chars, "_");
}
// Reduce an untrusted, possibly path-qualified name to a single sanitized file name.
// Returns an empty string when nothing usable remains.
inline std::string sanitize_file_basename(const std::string &name){
const size_t sep = name.find_last_of("/\\");
const std::string base = sanitize_filename(sep == std::string::npos ? name : name.substr(sep + 1));
// Names made only of dots and spaces refer to the folder or its parent, or are stripped to nothing on Windows.
return base.find_first_not_of(". ") == std::string::npos ? std::string() : base;
}
// Marker file a download of this process writes to before it is renamed to filename.
boost::filesystem::path download_marker_path(const boost::filesystem::path &dest_folder, const std::string &filename);
// Finds a sanitized variant of filename, "name(N).ext" if needed, that neither an entry of dest_folder
// nor the download marker of another download uses. The marker at ignored_marker does not count.
// Returns true and the name in result, or false and the last name tried.
bool find_unused_filename(const boost::filesystem::path &dest_folder, const std::string &filename,
const boost::filesystem::path &ignored_marker, std::string &result);
// File path / name / extension splitting utilities, working with UTF-8,
// to be published to Perl.
namespace PerlUtils {
+25
View File
@@ -1321,6 +1321,31 @@ 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
+16
View File
@@ -60,6 +60,22 @@ public:
// using the given camera matrices.
//
void render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix);
//
// ORCA: realistic view. Render the toolpaths as seen from the light, to fill the caller's
// shadow map. Depth only - the caller masks colour writes and owns the framebuffer.
//
void render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position);
//
// ORCA: realistic view. The shadow map the toolpaths sample, in the given texture unit.
// intensity == 0, the default, turns the lookup off and restores the plain shading.
//
void set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size);
//
// ORCA: tone applied to the shaded toolpaths, to pay back the light the lighting term,
// the shadow and the SSAO pass each take off. 1.0/1.0, the default, is a no-op; the
// caller decides which of the two it varies with the realistic view setting.
//
void set_tone(float exposure, float saturation);
//
// ************************************************************************
+20 -4
View File
@@ -15,7 +15,12 @@ namespace libvgcode {
//| 2--0-------5--7 |
//| \ | | / |
//| 3-------4 |
static constexpr const std::array<uint8_t, 24> VERTEX_DATA = {
// The eight corners the vertex shader knows how to place. Each is sent once and
// referenced by INDEX_DATA below, so the post-transform cache can reuse it across
// the triangles that share it: the shader runs 8 times per segment instead of 24.
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
0, 1, 2, // front spike
0, 2, 3, // front spike
0, 3, 4, // right/bottom body
@@ -31,7 +36,7 @@ void SegmentTemplate::init()
if (m_vao_id != 0)
return;
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t);
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
@@ -51,12 +56,22 @@ void SegmentTemplate::init()
glsafe(glVertexAttribIPointer(0, 1, GL_UNSIGNED_BYTE, 0, (const void*)0));
#endif // ENABLE_OPENGL_ES
// The element buffer binding is part of the vao state, so it is left bound here
// and restored together with the vao.
glsafe(glGenBuffers(1, &m_ibo_id));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
glsafe(glBindVertexArray(curr_vertex_array));
}
void SegmentTemplate::shutdown()
{
if (m_ibo_id != 0) {
glsafe(glDeleteBuffers(1, &m_ibo_id));
m_ibo_id = 0;
}
if (m_vbo_id != 0) {
glsafe(glDeleteBuffers(1, &m_vbo_id));
m_vbo_id = 0;
@@ -71,14 +86,15 @@ void SegmentTemplate::shutdown()
void SegmentTemplate::render(size_t count)
{
if (m_vao_id == 0 || m_vbo_id == 0 || count == 0)
if (m_vao_id == 0 || m_vbo_id == 0 || m_ibo_id == 0 || count == 0)
return;
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
glsafe(glBindVertexArray(m_vao_id));
glsafe(glDrawArraysInstanced(GL_TRIANGLES, 0, static_cast<GLsizei>(VERTEX_DATA.size()), static_cast<GLsizei>(count)));
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
nullptr, static_cast<GLsizei>(count)));
glsafe(glBindVertexArray(curr_vertex_array));
}
+1
View File
@@ -40,6 +40,7 @@ private:
//
unsigned int m_vao_id{ 0 };
unsigned int m_vbo_id{ 0 };
unsigned int m_ibo_id{ 0 };
//
// Size of the data sent to gpu, in bytes.
//
+68 -7
View File
@@ -16,7 +16,8 @@ static const char* Segments_Vertex_Shader =
"#define FIX_TWISTING\n"
"const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n"
"const float light_top_diffuse = 0.6 * 0.8;\n"
"const float light_top_specular = 0.6 * 0.125;\n"
// ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen.
"const float light_top_specular = 0.6 * 0.25;\n"
"const float light_top_shininess = 20.0;\n"
"const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);\n"
"const float light_front_diffuse = 0.6 * 0.2;\n"
@@ -30,8 +31,23 @@ static const char* Segments_Vertex_Shader =
"uniform samplerBuffer height_width_angle_tex;\n"
"uniform samplerBuffer color_tex;\n"
"uniform usamplerBuffer segment_index_tex;\n"
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
// world_position, so the caster would write a depth the receiver never looks up.
"uniform float bias_scale;\n"
// draw the instances last to first, top layers before the ones they hide, so that early depth
// rejection discards most of the hidden fragments; set when the camera looks down on the print
"uniform int reverse_order;\n"
"uniform int instance_count;\n"
"in int vertex_id;\n"
"out vec3 color;\n"
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
"// ambient and emissive terms in color, which a shadow does not occlude. Their sum is the\n"
"// single lighting term this replaces, so shading is unchanged while shadows are off.\n"
"out vec3 color_direct;\n"
"// ORCA: realistic view - the fragment shader looks the fragment up in the shadow map, which\n"
"// needs its world position and, for the depth bias, its eye space normal.\n"
"out vec3 world_position;\n"
"out vec3 shadow_normal;\n"
"vec3 decode_color(float color) {\n"
" int c = int(round(color));\n"
" int r = (c >> 16) & 0xFF;\n"
@@ -40,14 +56,15 @@ static const char* Segments_Vertex_Shader =
" float f = 1.0 / 255.0f;\n"
" return f * vec3(r, g, b);\n"
"}\n"
"float lighting(vec3 eye_position, vec3 eye_normal) {\n"
"float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n"
" float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n"
" float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);\n"
" float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);\n"
" return ambient + top_diffuse + front_diffuse + top_specular + emission;\n"
" return top_diffuse + front_diffuse + top_specular;\n"
"}\n"
"void main() {\n"
" int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n"
" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n"
" int id_a = int(texelFetch(segment_index_tex, instance).r);\n"
" int id_b = id_a + 1;\n"
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
@@ -135,19 +152,63 @@ static const char* Segments_Vertex_Shader =
" }\n"
" vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n"
" // ORCA: Apply bias to z-position to avoid z-fighting\n"
" eye_position.z += bias;\n"
" eye_position.z += bias * bias_scale;\n"
" vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n"
" vec3 color_base = decode_color(texelFetch(color_tex, id).r);\n"
" color = color_base * lighting(eye_position, eye_normal);\n"
" color = color_base * (ambient + emission);\n"
" color_direct = color_base * direct_lighting(eye_position, eye_normal);\n"
" world_position = pos;\n"
" shadow_normal = eye_normal;\n"
" gl_Position = projection_matrix * vec4(eye_position, 1.0);\n"
"}\n";
static const char* Segments_Fragment_Shader =
"#version 150\n"
"// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n"
"// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n"
"// so the toolpaths shade exactly as before whenever realistic view is off.\n"
"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n"
"uniform sampler2D shadow_map;\n"
"uniform mat4 shadow_light_vp;\n"
"uniform float shadow_intensity;\n"
"uniform float shadow_map_texel;\n"
// ORCA: the lighting term peaks near 0.9 and every later multiplier - the shadow, then the SSAO
// post pass - only takes more light away, so the print reads dimmer and duller than the legend
// colours. These pay that back. Both are 1.0 for an untouched image; what the caller actually
// passes in each mode is decided in GLCanvas3D::_render_gcode, not here.
"uniform float exposure;\n"
"uniform float saturation;\n"
"const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n"
"in vec3 color;\n"
"in vec3 color_direct;\n"
"in vec3 world_position;\n"
"in vec3 shadow_normal;\n"
"out vec4 fragment_color;\n"
"float shadow_shade() {\n"
" if (shadow_intensity <= 0.0)\n"
" return 1.0;\n"
" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n"
" vec3 proj = lp.xyz / lp.w;\n"
" proj = proj * 0.5 + 0.5;\n"
" if (proj.z > 1.0)\n"
" return 1.0;\n"
" // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n"
" // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n"
" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n"
" float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n"
" float sum = 0.0;\n"
" for (int x = -2; x <= 2; ++x) {\n"
" for (int y = -2; y <= 2; ++y) {\n"
" float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;\n"
" sum += (proj.z - bias > closest) ? 1.0 : 0.0;\n"
" }\n"
" }\n"
" return 1.0 - shadow_intensity * (sum / 25.0);\n"
"}\n"
"void main() {\n"
" fragment_color = vec4(color, 1.0);\n"
" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n"
" c = mix(vec3(dot(c, LUMA)), c, saturation);\n"
" fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n"
"}\n";
static const char* Options_Vertex_Shader =
+61 -5
View File
@@ -17,7 +17,8 @@ static const char* Segments_Vertex_Shader_ES =
"#define FIX_TWISTING\n"
"const vec3 light_top_dir = vec3(-0.4574957, 0.4574957, 0.7624929);\n"
"const float light_top_diffuse = 0.6 * 0.8;\n"
"const float light_top_specular = 0.6 * 0.125;\n"
// ORCA: the specular was 0.6 * 0.125, too faint to give the filament any sheen.
"const float light_top_specular = 0.6 * 0.25;\n"
"const float light_top_shininess = 20.0;\n"
"const vec3 light_front_dir = vec3(0.6985074, 0.1397015, 0.6985074);\n"
"const float light_front_diffuse = 0.6 * 0.3;\n"
@@ -33,6 +34,14 @@ static const char* Segments_Vertex_Shader_ES =
"uniform usampler2D segment_index_tex;\n"
"in float vertex_id_float;\n"
"out vec3 color;\n"
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
"// ambient and emissive terms in color, which a shadow does not occlude. Their sum is the\n"
"// single lighting term this replaces, so shading is unchanged while shadows are off.\n"
"out vec3 color_direct;\n"
"// ORCA: realistic view - the fragment shader looks the fragment up in the shadow map, which\n"
"// needs its world position and, for the depth bias, its eye space normal.\n"
"out vec3 world_position;\n"
"out vec3 shadow_normal;\n"
"vec3 decode_color(float color) {\n"
" int c = int(round(color));\n"
" int r = (c >> 16) & 0xFF;\n"
@@ -41,11 +50,11 @@ static const char* Segments_Vertex_Shader_ES =
" float f = 1.0 / 255.0f;\n"
" return f * vec3(r, g, b);\n"
"}\n"
"float lighting(vec3 eye_position, vec3 eye_normal) {\n"
"float direct_lighting(vec3 eye_position, vec3 eye_normal) {\n"
" float top_diffuse = light_top_diffuse * max(dot(eye_normal, light_top_dir), 0.0);\n"
" float front_diffuse = light_front_diffuse * max(dot(eye_normal, light_front_dir), 0.0);\n"
" float top_specular = light_top_specular * pow(max(dot(-normalize(eye_position), reflect(-light_top_dir, eye_normal)), 0.0), light_top_shininess);\n"
" return ambient + top_diffuse + front_diffuse + top_specular + emission;\n"
" return top_diffuse + front_diffuse + top_specular;\n"
"}\n"
"ivec2 tex_coord(sampler2D sampler, int id) {\n"
" ivec2 tex_size = textureSize(sampler, 0);\n"
@@ -143,17 +152,64 @@ static const char* Segments_Vertex_Shader_ES =
" vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n"
" vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n"
" vec3 color_base = decode_color(texelFetch(color_tex, tex_coord(color_tex, id), 0).r);\n"
" color = color_base * lighting(eye_position, eye_normal);\n"
" color = color_base * (ambient + emission);\n"
" color_direct = color_base * direct_lighting(eye_position, eye_normal);\n"
" world_position = pos;\n"
" shadow_normal = eye_normal;\n"
" gl_Position = projection_matrix * vec4(eye_position, 1.0);\n"
"}\n";
static const char* Segments_Fragment_Shader_ES =
"#version 300 es\n"
"precision highp float;\n"
"// ORCA: sampler2D defaults to lowp in an ES fragment shader, far too coarse to compare\n"
"// shadow map depths against.\n"
"precision highp sampler2D;\n"
"// ORCA: realistic view - object-on-object and self shadows, read from the same depth map the\n"
"// rest of the 3D scene samples. shadow_intensity == 0, the default, short-circuits the lookup,\n"
"// so the toolpaths shade exactly as before whenever realistic view is off.\n"
"const vec3 SHADOW_LIGHT_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);\n"
"uniform sampler2D shadow_map;\n"
"uniform mat4 shadow_light_vp;\n"
"uniform float shadow_intensity;\n"
"uniform float shadow_map_texel;\n"
// ORCA: the lighting term peaks near 0.9 and every later multiplier - the shadow, then the SSAO
// post pass - only takes more light away, so the print reads dimmer and duller than the legend
// colours. These pay that back. Both are 1.0 for an untouched image; what the caller actually
// passes in each mode is decided in GLCanvas3D::_render_gcode, not here.
"uniform float exposure;\n"
"uniform float saturation;\n"
"const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722);\n"
"in vec3 color;\n"
"in vec3 color_direct;\n"
"in vec3 world_position;\n"
"in vec3 shadow_normal;\n"
"out vec4 fragment_color;\n"
"float shadow_shade() {\n"
" if (shadow_intensity <= 0.0)\n"
" return 1.0;\n"
" vec4 lp = shadow_light_vp * vec4(world_position, 1.0);\n"
" vec3 proj = lp.xyz / lp.w;\n"
" proj = proj * 0.5 + 0.5;\n"
" if (proj.z > 1.0)\n"
" return 1.0;\n"
" // Slope-scaled bias, as in gouraud.fs. An extrusion is only a handful of shadow-map texels\n"
" // wide, so grazing faces need the larger bias to keep self-shadow acne off the top surfaces.\n"
" float NdotL = dot(normalize(shadow_normal), SHADOW_LIGHT_DIR);\n"
" float bias = mix(0.0004, 0.004, clamp(1.0 - NdotL, 0.0, 1.0));\n"
" float sum = 0.0;\n"
" for (int x = -2; x <= 2; ++x) {\n"
" for (int y = -2; y <= 2; ++y) {\n"
" float closest = texture(shadow_map, proj.xy + vec2(float(x), float(y)) * shadow_map_texel).r;\n"
" sum += (proj.z - bias > closest) ? 1.0 : 0.0;\n"
" }\n"
" }\n"
" return 1.0 - shadow_intensity * (sum / 25.0);\n"
"}\n"
"void main() {\n"
" fragment_color = vec4(color, 1.0);\n"
" vec3 c = (color + color_direct * shadow_shade()) * exposure;\n"
" c = mix(vec3(dot(c, LUMA)), c, saturation);\n"
" fragment_color = vec4(clamp(c, 0.0, 1.0), 1.0);\n"
"}\n";
static const char* Options_Vertex_Shader_ES =
+15
View File
@@ -42,6 +42,21 @@ void Viewer::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix)
m_impl->render(view_matrix, projection_matrix);
}
void Viewer::render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position)
{
m_impl->render_shadow_casters(view_matrix, projection_matrix, light_position);
}
void Viewer::set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size)
{
m_impl->set_shadow_map(texture_unit, light_view_projection, intensity, texel_size);
}
void Viewer::set_tone(float exposure, float saturation)
{
m_impl->set_tone(exposure, saturation);
}
EViewType Viewer::get_view_type() const
{
return m_impl->get_view_type();
+120 -4
View File
@@ -763,6 +763,16 @@ void ViewerImpl::init(const std::string& opengl_context_version)
m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex");
m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex");
m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex");
m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order");
m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count");
// ORCA: realistic view
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
m_uni_segments_shadow_intensity_id = glGetUniformLocation(m_segments_shader_id, "shadow_intensity");
m_uni_segments_shadow_map_texel_id = glGetUniformLocation(m_segments_shader_id, "shadow_map_texel");
m_uni_segments_exposure_id = glGetUniformLocation(m_segments_shader_id, "exposure");
m_uni_segments_saturation_id = glGetUniformLocation(m_segments_shader_id, "saturation");
m_uni_segments_bias_scale_id = glGetUniformLocation(m_segments_shader_id, "bias_scale");
glcheck();
assert(m_uni_segments_view_matrix_id != -1 &&
m_uni_segments_projection_matrix_id != -1 &&
@@ -875,6 +885,12 @@ void ViewerImpl::reset()
m_travels_time = { 0.0f, 0.0f };
m_vertices.clear();
m_vertices_colors.clear();
// swap rather than clear: these are sized by the print, and a reset means the memory
// should go back, not sit reserved until the next load
for (std::vector<float>& times : m_layer_start_times)
std::vector<float>().swap(times);
std::vector<uint32_t>().swap(m_layer_first_vertex);
std::vector<float>().swap(m_colors_scratch);
m_valid_lines_bitset.clear();
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
m_cog_marker.reset();
@@ -1048,6 +1064,37 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
v.layer_duration = m_layers.get_layer_time(m_settings.time_mode, static_cast<size_t>(v.layer_id));
}
// Index of the first vertex of each layer, walked back to front so that a layer with no
// vertex of its own inherits the next layer's index and the array stays non-decreasing.
if (!m_layers.empty()) {
const uint32_t vertices_count = static_cast<uint32_t>(m_vertices.size());
m_layer_first_vertex.assign(m_layers.count(), vertices_count);
for (uint32_t i = vertices_count; i > 0; --i) {
const uint32_t layer_id = m_vertices[i - 1].layer_id;
if (layer_id < m_layer_first_vertex.size())
m_layer_first_vertex[layer_id] = i - 1;
}
for (size_t i = m_layer_first_vertex.size() - 1; i > 0; --i)
m_layer_first_vertex[i - 1] = std::min(m_layer_first_vertex[i - 1], m_layer_first_vertex[i]);
// the running time at each layer's first vertex, summed in vertex order so that
// get_estimated_time_at() matches a full accumulation exactly
std::array<float, TIME_MODES_COUNT> running{};
for (std::vector<float>& times : m_layer_start_times)
times.assign(m_layer_first_vertex.size(), 0.0f);
size_t layer = 0;
for (size_t i = 0; i <= m_vertices.size(); ++i) {
for (; layer < m_layer_first_vertex.size() && m_layer_first_vertex[layer] == i; ++layer) {
for (size_t j = 0; j < TIME_MODES_COUNT; ++j)
m_layer_start_times[j][layer] = running[j];
}
if (i < m_vertices.size()) {
for (size_t j = 0; j < TIME_MODES_COUNT; ++j)
running[j] += m_vertices[i].times[j];
}
}
}
if (!m_layers.empty())
m_layers.set_view_range(0, static_cast<uint32_t>(m_layers.count()) - 1);
@@ -1261,7 +1308,10 @@ void ViewerImpl::update_colors_texture()
// Based on current settings and slider position, we might want to render some
// vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache).
std::vector<float> colors(m_vertices_colors.size());
// Reused across calls: this runs on every slider tick, and the allocation alone is
// 4 bytes per vertex of the whole print each time.
std::vector<float>& colors = m_colors_scratch;
colors.resize(m_vertices_colors.size());
assert(colors.size() == m_vertices.size() && m_vertices_colors.size() == m_vertices.size());
for (size_t i=0; i<m_vertices.size(); ++i) {
const PathVertex& v = m_vertices[i];
@@ -1321,7 +1371,7 @@ void ViewerImpl::update_colors()
m_settings.update_colors = false;
}
void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix)
void ViewerImpl::apply_pending_updates()
{
if (m_settings.update_view_full_range)
update_view_full_range();
@@ -1331,6 +1381,11 @@ void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matr
if (m_settings.update_colors)
update_colors();
}
void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix)
{
apply_pending_updates();
const Mat4x4 inv_view_matrix = inverse(view_matrix);
const Vec3 camera_position = { inv_view_matrix[12], inv_view_matrix[13], inv_view_matrix[14] };
@@ -1345,6 +1400,30 @@ void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matr
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
}
void ViewerImpl::render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position)
{
apply_pending_updates();
// Only the extrusions and travels cast: the option markers are indicators, not material.
m_rendering_shadow_casters = true;
render_segments(view_matrix, projection_matrix, light_position);
m_rendering_shadow_casters = false;
}
void ViewerImpl::set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size)
{
m_shadow_map_texture_unit = texture_unit;
m_shadow_light_vp = light_view_projection;
m_shadow_intensity = intensity;
m_shadow_map_texel = texel_size;
}
void ViewerImpl::set_tone(float exposure, float saturation)
{
m_exposure = exposure;
m_saturation = saturation;
}
void ViewerImpl::set_view_type(EViewType type)
{
m_settings.view_type = type;
@@ -1516,8 +1595,19 @@ void ViewerImpl::set_view_visible_range(Interval::value_type min, Interval::valu
float ViewerImpl::get_estimated_time_at(size_t id) const
{
return std::accumulate(m_vertices.begin(), m_vertices.begin() + id + 1, 0.0f,
[this](float a, const PathVertex& v) { return a + v.times[static_cast<size_t>(m_settings.time_mode)]; });
const size_t mode = static_cast<size_t>(m_settings.time_mode);
if (mode >= TIME_MODES_COUNT || id >= m_vertices.size())
return 0.0f;
size_t first = 0;
float time = 0.0f;
const size_t layer = static_cast<size_t>(m_vertices[id].layer_id);
if (layer < m_layer_first_vertex.size() && m_layer_first_vertex[layer] <= id) {
first = m_layer_first_vertex[layer];
time = m_layer_start_times[mode][layer];
}
for (size_t i = first; i <= id; ++i)
time += m_vertices[i].times[mode];
return time;
}
Color ViewerImpl::get_vertex_color(const PathVertex& v) const
@@ -1722,6 +1812,10 @@ size_t ViewerImpl::get_used_cpu_memory() const
ret += sizeof(m_extrusion_roles_colors);
ret += sizeof(m_options_colors);
ret += STDVEC_MEMSIZE(m_vertices, PathVertex);
for (const std::vector<float>& times : m_layer_start_times)
ret += STDVEC_MEMSIZE(times, float);
ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t);
ret += STDVEC_MEMSIZE(m_colors_scratch, float);
ret += m_valid_lines_bitset.size_in_bytes_cpu();
ret += m_height_range.size_in_bytes_cpu();
ret += m_width_range.size_in_bytes_cpu();
@@ -1787,7 +1881,11 @@ void ViewerImpl::update_view_full_range()
const bool travels_visible = m_settings.options_visibility[size_t(EOptionType::Travels)];
const bool wipes_visible = m_settings.options_visibility[size_t(EOptionType::Wipes)];
// every vertex before m_layer_first_vertex[layers_range[0]] has a smaller layer_id, so the loop
// below would skip all of them anyway
auto first_it = m_vertices.begin();
if (layers_range[0] < m_layer_first_vertex.size())
first_it += m_layer_first_vertex[layers_range[0]];
while (first_it != m_vertices.end() &&
(first_it->layer_id < layers_range[0] || !is_visible(*first_it, m_settings))) {
++first_it;
@@ -1994,6 +2092,24 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data()));
glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data()));
// The segments come in print order, bottom layer first. Seen from above, that is back to front,
// and every hidden fragment is shaded before the one that covers it. Drawing them last to first
// lets the depth test reject the hidden ones instead. The camera looks down when the world's
// up axis points towards it, which is the view matrix's (2, 2) entry being positive.
const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f;
glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0));
#ifndef ENABLE_OPENGL_ES
glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast<int>(m_enabled_segments_count)));
#endif // ENABLE_OPENGL_ES
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
// with the lookup off.
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
glsafe(glUniformMatrix4fv(m_uni_segments_shadow_light_vp_id, 1, GL_FALSE, m_shadow_light_vp.data()));
glsafe(glUniform1f(m_uni_segments_shadow_intensity_id, m_rendering_shadow_casters ? 0.0f : m_shadow_intensity));
glsafe(glUniform1f(m_uni_segments_shadow_map_texel_id, m_shadow_map_texel));
glsafe(glUniform1f(m_uni_segments_exposure_id, m_exposure));
glsafe(glUniform1f(m_uni_segments_saturation_id, m_saturation));
glsafe(glUniform1f(m_uni_segments_bias_scale_id, m_rendering_shadow_casters ? 0.0f : 1.0f));
glsafe(glDisable(GL_CULL_FACE));
+62
View File
@@ -71,6 +71,24 @@ public:
// Render the toolpaths
//
void render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix);
//
// ORCA: realistic view. Render the toolpaths as seen from the light, to fill the caller's
// shadow map. Only depth matters here, so the caller masks colour writes; light_position
// takes the place of the camera when the segment boxes are expanded, which gives their
// silhouette as the light sees it.
//
void render_shadow_casters(const Mat4x4& view_matrix, const Mat4x4& projection_matrix, const Vec3& light_position);
//
// ORCA: realistic view. The shadow map the toolpaths sample, in the given texture unit.
// intensity == 0, the default, turns the lookup off and restores the plain shading.
//
void set_shadow_map(int texture_unit, const Mat4x4& light_view_projection, float intensity, float texel_size);
//
// ORCA: tone applied to the shaded toolpaths, to pay back the light the lighting term,
// the shadow and the SSAO pass each take off. 1.0/1.0, the default, is a no-op; the
// caller decides which of the two it varies with the realistic view setting.
//
void set_tone(float exposure, float saturation);
EViewType get_view_type() const { return m_settings.view_type; }
void set_view_type(EViewType type);
@@ -234,6 +252,20 @@ private:
//
std::array<float, TIME_MODES_COUNT> m_total_time{ 0.0f, 0.0f };
//
// Running sum of the vertex estimated times at each layer's first vertex, for each time mode,
// so that get_estimated_time_at() only accumulates the vertices of one layer.
//
std::array<std::vector<float>, TIME_MODES_COUNT> m_layer_start_times;
//
// For each layer L, the index of the first vertex whose layer_id is >= L (m_vertices.size()
// if there is none). Derived from the vertices, so it stays exact whatever order they arrive in.
//
std::vector<uint32_t> m_layer_first_vertex;
//
// Scratch buffer for update_colors_texture(), kept alive across slider steps
//
std::vector<float> m_colors_scratch;
//
// Detected travel moves times
//
std::array<float, TIME_MODES_COUNT> m_travels_time{ 0.0f, 0.0f };
@@ -330,6 +362,15 @@ private:
int m_uni_segments_height_width_angle_tex_id{ -1 };
int m_uni_segments_colors_tex_id{ -1 };
int m_uni_segments_segment_index_tex_id{ -1 };
int m_uni_segments_reverse_order_id{ -1 };
int m_uni_segments_instance_count_id{ -1 };
int m_uni_segments_shadow_map_id{ -1 };
int m_uni_segments_shadow_light_vp_id{ -1 };
int m_uni_segments_shadow_intensity_id{ -1 };
int m_uni_segments_shadow_map_texel_id{ -1 };
int m_uni_segments_exposure_id{ -1 };
int m_uni_segments_saturation_id{ -1 };
int m_uni_segments_bias_scale_id{ -1 };
//
// Caches for OpenGL uniforms id for options shader
//
@@ -469,6 +510,27 @@ private:
size_t m_enabled_options_tex_size{ 0 };
#endif // ENABLE_OPENGL_ES
//
// ORCA: realistic view. Shadow map state set by set_shadow_map(), consumed by the segments
// shader. m_rendering_shadow_casters forces the intensity to 0 for the depth pass, which
// must not sample the very map it is writing.
//
// Defaults past the four texture units render_segments() binds itself, so the sampler never
// aliases one of the buffer textures before the owner of the map has said where it lives.
int m_shadow_map_texture_unit{ 4 };
Mat4x4 m_shadow_light_vp{ 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f };
float m_shadow_intensity{ 0.0f };
float m_shadow_map_texel{ 0.0f };
bool m_rendering_shadow_casters{ false };
//
// ORCA: realistic view. Tone set by set_tone(), consumed by the segments shader.
// The identity values leave the shading as it is outside realistic view.
//
float m_exposure{ 1.0f };
float m_saturation{ 1.0f };
void apply_pending_updates();
void update_view_full_range();
void update_color_ranges();
void update_heights_widths();
+18 -2
View File
@@ -139,8 +139,16 @@ set(SLIC3R_GUI_SOURCES
GUI/TerminalDialog.hpp
GUI/PluginProgressDialog.cpp
GUI/PluginProgressDialog.hpp
GUI/PluginWebDialog.cpp
GUI/PluginWebDialog.hpp
GUI/WebDialog.cpp
GUI/WebDialog.hpp
GUI/DockPanel.cpp
GUI/DockPanel.hpp
GUI/WebPanel.cpp
GUI/WebPanel.hpp
GUI/Widgets/WebHosting.cpp
GUI/Widgets/WebHosting.hpp
GUI/AuiPaneLayout.cpp
GUI/AuiPaneLayout.hpp
GUI/DragCanvas.cpp
GUI/DragCanvas.hpp
GUI/EditGCodeDialog.cpp
@@ -280,6 +288,8 @@ set(SLIC3R_GUI_SOURCES
GUI/FilamentMapDialog.hpp
GUI/IconManager.cpp
GUI/IconManager.hpp
GUI/IdleScheduler.cpp
GUI/IdleScheduler.hpp
GUI/ImageGrid.cpp
GUI/ImageGrid.h
GUI/ImGuiWrapper.cpp
@@ -349,6 +359,10 @@ set(SLIC3R_GUI_SOURCES
GUI/KBShortcutsDialog.hpp
GUI/KeyChord.cpp
GUI/KeyChord.hpp
GUI/Lazy.cpp
GUI/Lazy.hpp
GUI/LazyPage.cpp
GUI/LazyPage.hpp
GUI/LibVGCode/LibVGCodeWrapper.hpp
GUI/LibVGCode/LibVGCodeWrapper.cpp
GUI/LinuxDisplayBackend.cpp
@@ -435,6 +449,7 @@ set(SLIC3R_GUI_SOURCES
GUI/Plater.hpp
GUI/PlateSettingsDialog.cpp
GUI/PlateSettingsDialog.hpp
GUI/PrebuildQueue.hpp
GUI/Preferences.cpp
GUI/Preferences.hpp
GUI/PresetBundleDialog.cpp
@@ -514,6 +529,7 @@ set(SLIC3R_GUI_SOURCES
GUI/SliceInfoPanel.hpp
GUI/SlicingProgressNotification.cpp
GUI/SlicingProgressNotification.hpp
GUI/StagedBuild.hpp
GUI/StatusPanel.cpp
GUI/StatusPanel.hpp
GUI/StepMeshDialog.cpp
+6
View File
@@ -625,6 +625,12 @@ void Bed3D::update_model_offset()
shift(2) = -0.03;
Vec3d* model_offset_ptr = const_cast<Vec3d*>(&m_model_offset);
*model_offset_ptr = shift;
//BBS: TODO: hack for current stl for BBL printer
if (std::string::npos != m_model_filename.find("bbl-3dp-"))
{
(*model_offset_ptr)(0) -= m_bed_shape[2].x() / 2.0f;
(*model_offset_ptr)(1) -= m_bed_shape[2].y() / 2.0f;
}
(*model_offset_ptr)(2) = -0.41 + GROUND_Z;
// update extended bounding box
+30
View File
@@ -162,6 +162,14 @@ ColorRGBA GLVolume::SUPPORT_BLOCKER_COL = {1.0f, 0.3f, 0.3f, 0.4f};
ColorRGBA GLVolume::MODEL_HIDDEN_COL = {0.f, 0.f, 0.f, 0.3f};
// Precise Seam modifier colors
ColorRGBA GLVolume::PRECISE_SEAM_CENTER_COL = {1.0f, 0.627f, 0.082f, 0.6f}; // FFA015 - orange
ColorRGBA GLVolume::PRECISE_SEAM_LEFT_COL = {1.0f, 0.753f, 0.0f, 0.6f}; // FFC000 - golden
ColorRGBA GLVolume::PRECISE_SEAM_RIGHT_COL = {1.0f, 0.514f, 0.0f, 0.6f}; // FF8300 - dark orange
ColorRGBA GLVolume::PRECISE_SEAM_ENFORCED_COL = {0.412f, 0.820f, 0.412f, 0.6f}; // 69D169 - green
ColorRGBA GLVolume::PRECISE_SEAM_NEUTRAL_COL = {0.655f, 0.655f, 0.655f, 0.6f}; // A7A7A7 - gray
ColorRGBA GLVolume::PRECISE_SEAM_BLOCKED_COL = {0.820f, 0.412f, 0.412f, 0.6f}; // D16969 - red
std::array<ColorRGBA, 5> GLVolume::MODEL_COLOR = { {
{ 1.0f, 1.0f, 0.0f, 1.f },
{ 1.0f, 0.5f, 0.5f, 1.f },
@@ -363,6 +371,28 @@ ColorRGBA color_from_model_volume(const ModelVolume& model_volume)
ColorRGBA color;
if (model_volume.is_negative_volume())
return GLVolume::MODEL_NEGTIVE_COL;
else if (model_volume.is_precise_seam()) {
// Return color based on Precise Seam subtype.
// Exhaustive switch (no default) so -Wswitch flags any future PRECISE_SEAM_* additions.
switch (model_volume.type()) {
case ModelVolumeType::PRECISE_SEAM_CENTER: return GLVolume::PRECISE_SEAM_CENTER_COL;
case ModelVolumeType::PRECISE_SEAM_LEFT: return GLVolume::PRECISE_SEAM_LEFT_COL;
case ModelVolumeType::PRECISE_SEAM_RIGHT: return GLVolume::PRECISE_SEAM_RIGHT_COL;
case ModelVolumeType::PRECISE_SEAM_ENFORCED: return GLVolume::PRECISE_SEAM_ENFORCED_COL;
case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return GLVolume::PRECISE_SEAM_NEUTRAL_COL;
case ModelVolumeType::PRECISE_SEAM_BLOCKED: return GLVolume::PRECISE_SEAM_BLOCKED_COL;
// Non-seam types are unreachable due to the outer is_precise_seam() guard;
// listed explicitly so this switch stays exhaustive over ModelVolumeType.
case ModelVolumeType::INVALID:
case ModelVolumeType::MODEL_PART:
case ModelVolumeType::NEGATIVE_VOLUME:
case ModelVolumeType::PARAMETER_MODIFIER:
case ModelVolumeType::SUPPORT_BLOCKER:
case ModelVolumeType::SUPPORT_ENFORCER:
break;
}
return GLVolume::MODEL_MIDIFIER_COL; // unreachable fallback
}
else if (model_volume.is_modifier())
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
return GLVolume::MODEL_MIDIFIER_COL;
+7
View File
@@ -93,6 +93,13 @@ public:
static ColorRGBA SUPPORT_ENFORCER_COL;
static ColorRGBA SUPPORT_BLOCKER_COL;
static ColorRGBA MODEL_HIDDEN_COL;
// Precise Seam modifier colors
static ColorRGBA PRECISE_SEAM_CENTER_COL;
static ColorRGBA PRECISE_SEAM_LEFT_COL;
static ColorRGBA PRECISE_SEAM_RIGHT_COL;
static ColorRGBA PRECISE_SEAM_ENFORCED_COL;
static ColorRGBA PRECISE_SEAM_NEUTRAL_COL;
static ColorRGBA PRECISE_SEAM_BLOCKED_COL;
static void update_render_colors();
static void load_render_colors();
+4 -4
View File
@@ -819,7 +819,7 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) :
m_scrolled_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL);
m_scrolled_window->SetBackgroundColour(*wxWHITE);
m_scrolled_window->SetScrollRate(0, FromDIP(10));
m_scrolled_window->SetScrollRate(0, FromDIP(20));
wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL);
wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL);
@@ -1000,13 +1000,13 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) :
show_pos.x = screen_size.GetLeft();
m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height));
m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height));
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(10));
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(20));
}
else
{
m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height));
m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height));
m_scrolled_window->SetScrollRate(0, FromDIP(10));
m_scrolled_window->SetScrollRate(0, FromDIP(20));
}
//Vertical Direction Processing
@@ -2117,7 +2117,7 @@ void AmsReplaceMaterialDialog::create()
identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green)
m_scrollview_groups = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrollview_groups->SetScrollRate(5, 5);
m_scrollview_groups->SetScrollRate(5, FromDIP(20));
//m_scrollview_groups->SetMinSize(wxSize(400, 400));
//m_scrollview_groups->SetMaxSize(wxSize(400, 400));
m_scrollview_sizer = new wxBoxSizer(wxVERTICAL);
+20
View File
@@ -0,0 +1,20 @@
#include "AuiPaneLayout.hpp"
namespace Slic3r { namespace GUI {
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name)
{
// Panes are separated by '|'; SavePerspective() escapes a '|' inside a caption as "\|".
const std::string prefix = "name=" + pane_name + ";";
size_t begin = 0;
for (size_t i = 0; i <= layout.size(); ++i) {
if (i < layout.size() && (layout[i] != '|' || (i > 0 && layout[i - 1] == '\\')))
continue;
if (layout.compare(begin, prefix.size(), prefix) == 0)
return layout.substr(begin, i - begin);
begin = i + 1;
}
return {};
}
}} // namespace Slic3r::GUI
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace Slic3r { namespace GUI {
// The part a wxAuiManager layout string (wxAuiManager::SavePerspective) holds for `pane_name`, in the
// form wxAuiManager::LoadPaneInfo() takes, or empty when the layout has no such pane.
std::string aui_pane_layout_entry(const std::string& layout, const std::string& pane_name);
}} // namespace Slic3r::GUI
+22 -12
View File
@@ -606,7 +606,7 @@ AuFolderPanel::AuFolderPanel(wxWindow *parent, AuxiliaryFolderType type, wxWindo
wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 5);
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL);
wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL);
@@ -863,17 +863,27 @@ void AuxiliaryPanel::init_tabpanel()
m_tabpanel->SetBackgroundColour(wxColour("#FEFFFF"));
m_tabpanel->Bind(wxEVT_BOOKCTRL_PAGE_CHANGED, [](wxBookCtrlEvent &e) { /* Event handling */ });
m_designer_panel = new DesignerPanel(m_tabpanel, AuxiliaryFolderType::DESIGNER);
m_pictures_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::MODEL_PICTURE);
m_bill_of_materials_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::BILL_OF_MATERIALS);
m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE);
m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS);
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true);
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false);
m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false);
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false);
m_tabpanel->AddPage(m_others_panel, _L("Others"), false);
add_build_step([this] {
m_designer_panel = new DesignerPanel(m_tabpanel, AuxiliaryFolderType::DESIGNER);
m_tabpanel->AddPage(m_designer_panel, _L("Basic Info"), true);
});
add_build_step([this] {
m_pictures_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::MODEL_PICTURE);
m_tabpanel->AddPage(m_pictures_panel, _L("Pictures"), false);
});
add_build_step([this] {
m_bill_of_materials_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::BILL_OF_MATERIALS);
m_tabpanel->AddPage(m_bill_of_materials_panel, _L("Bill of Materials"), false);
});
add_build_step([this] {
m_assembly_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::ASSEMBLY_GUIDE);
m_tabpanel->AddPage(m_assembly_panel, _L("Assembly Guide"), false);
});
add_build_step([this] {
m_others_panel = new AuFolderPanel(m_tabpanel, AuxiliaryFolderType::OTHERS);
m_tabpanel->AddPage(m_others_panel, _L("Others"), false);
Layout();
});
}
wxWindow *AuxiliaryPanel::create_side_tools()
+2 -1
View File
@@ -46,6 +46,7 @@
#include "slic3r/GUI/UpgradePanel.hpp"
#include "slic3r/GUI/AmsWidgets.hpp"
#include "Widgets/SideTools.hpp"
#include "StagedBuild.hpp"
#define AUFILE_GREY700 wxColour(107, 107, 107)
#define AUFILE_GREY500 wxColour(158, 158, 158)
@@ -194,7 +195,7 @@ public:
};
class AuxiliaryPanel : public wxPanel
class AuxiliaryPanel : public wxPanel, public StagedBuild
{
private:
Tabbook *m_tabpanel = {nullptr};
+1 -1
View File
@@ -558,7 +558,7 @@ PingCodeBindDialog::~PingCodeBindDialog() {
//show bind failed info
m_sw_bind_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL);
m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_bind_failed_info->SetScrollRate(5, 5);
m_sw_bind_failed_info->SetScrollRate(5, FromDIP(20));
m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90)));
m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90)));
+5 -5
View File
@@ -3521,7 +3521,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
cards->Show(m_box_mate, false, true);
m_form->FitInside();
m_form->SetScrollRate(0, 10); // vertical only, like Prepare's sidebar: never scroll labels out
m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out
m_form->SetMinSize(wxSize(264, -1));
// Right column: a small view toolbar over the live 3D viewport that mirrors
@@ -4862,13 +4862,13 @@ void DesignPanel::on_import_mesh()
try {
shape = GeometryEngine::mesh_to_brep(mesh.its, MESH_IMPORT_TOLERANCE,
MESH_IMPORT_MERGE_ANGLE_DEG, stats);
} catch (const std::exception& e) {
fail(_L("Mesh conversion failed: ") + wxString::FromUTF8(e.what()));
return;
} catch (const Standard_Failure& e) { // OCCT throws outside std::exception
} catch (const Standard_Failure& e) { // on OCCT >= 8 Standard_Failure derives from std::exception — must precede that handler
fail(_L("Mesh conversion failed: ") + wxString::FromUTF8(
e.GetMessageString() ? e.GetMessageString() : "OCCT error"));
return;
} catch (const std::exception& e) {
fail(_L("Mesh conversion failed: ") + wxString::FromUTF8(e.what()));
return;
}
if (shape.IsNull()) { fail(_L("Mesh conversion produced no geometry")); return; }
+2 -1
View File
@@ -12,6 +12,7 @@
#include "libslic3r/CAD/CadDocument.hpp"
#include "slic3r/GUI/CAD/DesignInteraction.hpp" // CadLevel: what one Esc press means
#include "slic3r/GUI/Lazy.hpp"
class ComboBox; // Orca dropdown (Widgets/ComboBox.hpp) — replaces wxChoice everywhere here
class StaticBox; // Orca rounded card frame (Widgets/StaticBox.hpp)
@@ -46,7 +47,7 @@ class DesignCanvas;
// Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel.
// Sketch and Extrude are independent tools: the user creates a Sketch first,
// then selects it and Extrudes to produce a solid.
class DesignPanel : public wxPanel
class DesignPanel : public wxPanel, public LazyInstance<DesignPanel>
{
public:
explicit DesignPanel(wxWindow* parent);
+1 -2
View File
@@ -1989,11 +1989,10 @@ json action_set_feature_expr(DesignPanel* panel, const json& params)
// Dispatch one parsed request ON THE MAIN THREAD. Returns a JSON-RPC reply string.
std::string handle_on_main(const std::string& method, const json& params, const json& id)
{
MainFrame* mf = wxGetApp().mainframe;
// The panel is built on first use, and in a headless session nobody clicks the tab that
// would build it -- so build it here rather than refusing. Safe: this runs on the main
// thread (see the CallAfter that dispatches us).
DesignPanel* panel = mf ? mf->ensure_design_panel() : nullptr;
DesignPanel* panel = DesignPanel::ensure();
if (!panel)
return rpc_error(id, -32001, "Design panel not ready");
+11 -7
View File
@@ -115,7 +115,7 @@ HistoryWindow::HistoryWindow(wxWindow* parent, const std::vector<PACalibResult>&
auto main_sizer = new wxBoxSizer(wxVERTICAL);
auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
scroll_window->SetScrollRate(5, 5);
scroll_window->SetScrollRate(5, FromDIP(20));
scroll_window->SetBackgroundColour(*wxWHITE);
scroll_window->SetMinSize(HISTORY_WINDOW_SIZE);
scroll_window->SetSize(HISTORY_WINDOW_SIZE);
@@ -818,12 +818,14 @@ NewCalibrationHistoryDialog::NewCalibrationHistoryDialog(wxWindow *parent, const
if (support_nozzle_volume(curr_obj)) {
Label *nozzle_name_title = new Label(top_panel, _L("Nozzle"));
m_comboBox_nozzle_type = new ::ComboBox(top_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, NEW_HISTORY_DIALOG_INPUT_SIZE, 0, nullptr, wxCB_READONLY);
wxArrayString nozzle_items;
// The labels are in display order, not enum order (E3D High Flow is 5 but the fifth label), so each
// item carries its NozzleVolumeType as client data.
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
for (auto item : nozzle_volume_type_def->enum_labels) { nozzle_items.push_back(_L(item)); }
for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i)
m_comboBox_nozzle_type->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap,
(void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i]));
}
m_comboBox_nozzle_type->Set(nozzle_items);
m_comboBox_nozzle_type->SetSelection(-1);
flex_sizer->Add(nozzle_name_title);
flex_sizer->Add(m_comboBox_nozzle_type);
@@ -891,7 +893,7 @@ int NewCalibrationHistoryDialog::get_nozzle_combo_id_code() const
void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event)
{
// Mirror the picked hotend's flow onto the (Orca index-based) nozzle-type combo.
// Mirror the picked hotend's flow onto the nozzle-type combo.
if (!curr_obj || !m_comboBox_nozzle_id || !m_comboBox_nozzle_type || !curr_obj->GetNozzleSystem())
return;
@@ -902,7 +904,9 @@ void NewCalibrationHistoryDialog::on_select_nozzle_pos(wxCommandEvent &event)
DevNozzle nozzle = curr_obj->GetNozzleSystem()->GetNozzleByPosId(pos);
if (nozzle.IsNormal()) {
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.GetNozzleFlowType());
m_comboBox_nozzle_type->SetSelection(static_cast<int>(volume_type));
for (unsigned int i = 0; i < m_comboBox_nozzle_type->GetCount(); ++i)
if (NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(i))) == volume_type)
m_comboBox_nozzle_type->SetSelection(i);
}
}
@@ -949,7 +953,7 @@ void NewCalibrationHistoryDialog::on_ok(wxCommandEvent &event)
msg_dlg.ShowModal();
return;
}
m_new_result.nozzle_volume_type = NozzleVolumeType(m_comboBox_nozzle_type->GetSelection());
m_new_result.nozzle_volume_type = NozzleVolumeType(intptr_t(m_comboBox_nozzle_type->GetClientData(m_comboBox_nozzle_type->GetSelection())));
}
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
+7 -4
View File
@@ -207,7 +207,7 @@ SelectMObjectPopup::SelectMObjectPopup(wxWindow* parent)
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
m_scrolledWindow->SetScrollRate(0, 5);
m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
m_scrolledWindow->Layout();
@@ -491,14 +491,15 @@ void CalibrationPanel::init_tabpanel() {
selected);
}
for (int i = 0; i < (int)CALI_MODE_COUNT; i++)
add_build_steps_of(*m_cali_panels[i]);
// ORCA use standard paddings and keep arrow icon for consistent look between sidebars
//for (int i = 0; i < (int)CALI_MODE_COUNT; i++)
// m_tabpanel->SetPageImage(i, "");
//auto padding_size = m_tabpanel->GetBtnsListCtrl()->GetPaddingSize(0);
//m_tabpanel->GetBtnsListCtrl()->SetPaddingSize({ FromDIP(15), padding_size.y });
m_initialized = true;
}
void CalibrationPanel::init_timer()
@@ -534,6 +535,8 @@ void CalibrationPanel::update_print_error_info(int code, std::string msg, std::s
}
void CalibrationPanel::update_all() {
// Every wizard's pages exist once the last build step has run.
if (!built()) return;
NetworkAgent* m_agent = wxGetApp().getAgent();
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
@@ -597,7 +600,7 @@ void CalibrationPanel::update_all() {
void CalibrationPanel::show_status(int status)
{
if (!m_initialized) return;
if (!built()) return;
if (last_status == status)return;
last_status = status;
+2 -2
View File
@@ -3,6 +3,7 @@
#include "CalibrationWizard.hpp"
#include "Tabbook.hpp"
#include "Lazy.hpp"
//#include "Widgets/SideTools.hpp"
namespace Slic3r { namespace GUI {
@@ -88,7 +89,7 @@ private:
};
class CalibrationPanel : public wxPanel
class CalibrationPanel : public wxPanel, public StagedBuild, public LazyInstance<CalibrationPanel>
{
public:
CalibrationPanel(wxWindow* parent, wxWindowID id = wxID_ANY, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = wxTAB_TRAVERSAL);
@@ -109,7 +110,6 @@ protected:
int last_status;
bool m_initialized { false };
std::string last_conn_type = "undedefined";
MachineObject* obj{ nullptr };
MachineObject* last_obj { nullptr };
+57 -66
View File
@@ -81,7 +81,7 @@ CalibrationWizard::CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowI
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 5);
m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_scrolledWindow->SetBackgroundColour(*wxWHITE);
wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL);
@@ -130,6 +130,15 @@ CalibrationWizard::~CalibrationWizard()
;
}
void CalibrationWizard::add_page_step(CalibrationWizardPageStep*& step, std::function<CalibrationWizardPage*()> make)
{
add_build_step([this, &step, make = std::move(make)] {
step = new CalibrationWizardPageStep(make());
m_all_pages_sizer->Add(step->page, 1, wxEXPAND | wxALL, FromDIP(25));
step->page->Hide();
});
}
void CalibrationWizard::on_cali_job_finished(wxCommandEvent& event)
{
this->on_cali_job_finished(event.GetString());
@@ -520,33 +529,28 @@ void PressureAdvanceWizard::on_cali_job_finished(wxString evt_data)
void PressureAdvanceWizard::create_pages()
{
start_step = new CalibrationWizardPageStep(new CalibrationPAStartPage(m_scrolledWindow));
preset_step = new CalibrationWizardPageStep(new CalibrationPresetPage(m_scrolledWindow, m_mode, false));
cali_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode));
save_step = new CalibrationWizardPageStep(new CalibrationPASavePage(m_scrolledWindow));
add_page_step(start_step, [this] { return new CalibrationPAStartPage(m_scrolledWindow); });
add_page_step(preset_step, [this] { return new CalibrationPresetPage(m_scrolledWindow, m_mode, false); });
add_page_step(cali_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode); });
add_page_step(save_step, [this] { return new CalibrationPASavePage(m_scrolledWindow); });
m_all_pages_sizer->Add(start_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(preset_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(cali_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(save_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
add_build_step([this] {
m_page_steps.push_back(start_step);
m_page_steps.push_back(preset_step);
m_page_steps.push_back(cali_step);
m_page_steps.push_back(save_step);
for (int i = 0; i < m_page_steps.size() -1; i++) {
m_page_steps[i]->chain(m_page_steps[i+1]);
}
m_page_steps.push_back(start_step);
m_page_steps.push_back(preset_step);
m_page_steps.push_back(cali_step);
m_page_steps.push_back(save_step);
for (int i = 0; i < m_page_steps.size(); i++) {
m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &PressureAdvanceWizard::on_cali_action, this);
}
for (int i = 0; i < m_page_steps.size() -1; i++) {
m_page_steps[i]->chain(m_page_steps[i+1]);
}
for (int i = 0; i < m_page_steps.size(); i++) {
m_page_steps[i]->page->Hide();
m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &PressureAdvanceWizard::on_cali_action, this);
}
if (!m_page_steps.empty())
show_step(m_page_steps.front());
if (!m_page_steps.empty())
show_step(m_page_steps.front());
});
}
void PressureAdvanceWizard::on_cali_action(wxCommandEvent& evt)
@@ -1053,59 +1057,46 @@ FlowRateWizard::FlowRateWizard(wxWindow* parent, wxWindowID id, const wxPoint& p
void FlowRateWizard::create_pages()
{
start_step = new CalibrationWizardPageStep(new CalibrationFlowRateStartPage(m_scrolledWindow));
preset_step = new CalibrationWizardPageStep(new CalibrationPresetPage(m_scrolledWindow, m_mode, false));
add_page_step(start_step, [this] { return new CalibrationFlowRateStartPage(m_scrolledWindow); });
add_page_step(preset_step, [this] { return new CalibrationPresetPage(m_scrolledWindow, m_mode, false); });
// manual
cali_coarse_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_CALI));
coarse_save_step = new CalibrationWizardPageStep(new CalibrationFlowCoarseSavePage(m_scrolledWindow));
cali_fine_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_FINE_CALI));
fine_save_step = new CalibrationWizardPageStep(new CalibrationFlowFineSavePage(m_scrolledWindow));
add_page_step(cali_coarse_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_CALI); });
add_page_step(coarse_save_step, [this] { return new CalibrationFlowCoarseSavePage(m_scrolledWindow); });
add_page_step(cali_fine_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode, CaliPageType::CALI_PAGE_FINE_CALI); });
add_page_step(fine_save_step, [this] { return new CalibrationFlowFineSavePage(m_scrolledWindow); });
// auto
cali_step = new CalibrationWizardPageStep(new CalibrationCaliPage(m_scrolledWindow, m_mode));
save_step = new CalibrationWizardPageStep(new CalibrationFlowX1SavePage(m_scrolledWindow));
add_page_step(cali_step, [this] { return new CalibrationCaliPage(m_scrolledWindow, m_mode); });
add_page_step(save_step, [this] { return new CalibrationFlowX1SavePage(m_scrolledWindow); });
m_all_pages_sizer->Add(start_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(preset_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(cali_coarse_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(coarse_save_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(cali_fine_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(fine_save_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
add_build_step([this] {
m_page_steps.push_back(start_step);
m_page_steps.push_back(preset_step);
m_page_steps.push_back(cali_coarse_step);
m_page_steps.push_back(coarse_save_step);
m_page_steps.push_back(cali_fine_step);
m_page_steps.push_back(fine_save_step);
m_all_pages_sizer->Add(cali_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
m_all_pages_sizer->Add(save_step->page, 1, wxEXPAND | wxALL, FromDIP(25));
//m_page_steps.push_back(cali_step);
//m_page_steps.push_back(save_step);
m_page_steps.push_back(start_step);
m_page_steps.push_back(preset_step);
m_page_steps.push_back(cali_coarse_step);
m_page_steps.push_back(coarse_save_step);
m_page_steps.push_back(cali_fine_step);
m_page_steps.push_back(fine_save_step);
for (int i = 0; i < m_page_steps.size() - 1; i++) {
m_page_steps[i]->chain(m_page_steps[i + 1]);
}
//m_page_steps.push_back(cali_step);
//m_page_steps.push_back(save_step);
for (int i = 0; i < m_page_steps.size(); i++) {
m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this);
}
for (int i = 0; i < m_page_steps.size() - 1; i++) {
m_page_steps[i]->chain(m_page_steps[i + 1]);
}
cali_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this);
save_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this);
// hide all pages
cali_step->page->Hide();
save_step->page->Hide();
for (int i = 0; i < m_page_steps.size(); i++) {
m_page_steps[i]->page->Hide();
m_page_steps[i]->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this);
}
if (!m_page_steps.empty())
show_step(m_page_steps.front());
cali_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this);
save_step->page->Bind(EVT_CALI_ACTION, &FlowRateWizard::on_cali_action, this);
if (!m_page_steps.empty())
show_step(m_page_steps.front());
set_cali_method(CalibrationMethod::CALI_METHOD_MANUAL);
set_cali_method(CalibrationMethod::CALI_METHOD_MANUAL);
});
}
void FlowRateWizard::on_cali_action(wxCommandEvent& evt)
+5 -1
View File
@@ -9,6 +9,7 @@
#include "CalibrationWizardPresetPage.hpp"
#include "CalibrationWizardCaliPage.hpp"
#include "CalibrationWizardSavePage.hpp"
#include "StagedBuild.hpp"
namespace Slic3r { namespace GUI {
@@ -36,7 +37,7 @@ struct ConfigIndexValue
int index{0};
};
class CalibrationWizard : public wxPanel {
class CalibrationWizard : public wxPanel, public StagedBuild {
public:
CalibrationWizard(wxWindow* parent, CalibMode mode,
wxWindowID id = wxID_ANY,
@@ -79,6 +80,9 @@ public:
protected:
void on_cali_go_home();
// Queues a page as a build step, created hidden and added to the pages sizer.
void add_page_step(CalibrationWizardPageStep*& step, std::function<CalibrationWizardPage*()> make);
protected:
/* wx widgets*/
wxScrolledWindow* m_scrolledWindow;
+3 -1
View File
@@ -192,8 +192,10 @@ void CalibrationCaliPage::update(MachineObject* obj)
set_cali_img();
}
// A calibration can run before the Device tab is ever opened, and only its status
// panel shows a print error.
if (obj->print_error > 0) {
StatusPanel* status_panel = Slic3r::GUI::wxGetApp().mainframe->m_monitor->get_status_panel();
StatusPanel* status_panel = MonitorPanel::ensure()->get_status_panel();
status_panel->obj = obj;
status_panel->update_error_message();
}
+1 -1
View File
@@ -834,7 +834,7 @@ void CaliPageSendingPanel::create(wxWindow* parent)
m_sw_print_failed_info = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5);
m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
+32 -33
View File
@@ -803,6 +803,29 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent)
#define NOZZLE_LIST_DEFAULT 1
float nozzle_diameter_list[NOZZLE_LIST_COUNT] = {0.2, 0.4, 0.6, 0.8 };
// The nozzle_volume_type labels are in display order, not enum order (E3D High Flow is 5 but the fifth
// label), so each item carries its NozzleVolumeType as client data and is selected by that value.
static void select_nozzle_volume(ComboBox *combo, NozzleVolumeType volume_type)
{
for (unsigned int i = 0; i < combo->GetCount(); ++i)
if (NozzleVolumeType(intptr_t(combo->GetClientData(i))) == volume_type) {
combo->SetSelection(i);
return;
}
}
static void fill_nozzle_volume_combo(ComboBox *combo)
{
combo->Clear();
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
for (size_t i = 0; i < nozzle_volume_type_def->enum_labels.size(); ++i)
combo->Append(_L(nozzle_volume_type_def->enum_labels[i]), wxNullBitmap,
(void *) (intptr_t) nozzle_volume_type_def->enum_keys_map->at(nozzle_volume_type_def->enum_values[i]));
}
select_nozzle_volume(combo, NozzleVolumeType::nvtStandard);
}
void CalibrationPresetPage::init_selection_values()
{
// init nozzle diameter and nozzle volume
@@ -813,15 +836,7 @@ void CalibrationPresetPage::init_selection_values()
}
m_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT);
m_comboBox_nozzle_volume->Clear();
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
for (auto item : nozzle_volume_type_def->enum_labels) {
m_comboBox_nozzle_volume->AppendString(_L(item));
}
}
m_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard));
fill_nozzle_volume_combo(m_comboBox_nozzle_volume);
}
Preset* cur_printer_preset = get_printer_preset(curr_obj, 0.4);
@@ -866,15 +881,7 @@ void CalibrationPresetPage::init_selection_values()
}
m_left_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT);
m_left_comboBox_nozzle_volume->Clear();
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
for (auto item : nozzle_volume_type_def->enum_labels) {
m_left_comboBox_nozzle_volume->AppendString(_L(item));
}
}
m_left_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard));
fill_nozzle_volume_combo(m_left_comboBox_nozzle_volume);
}
// right
@@ -885,15 +892,7 @@ void CalibrationPresetPage::init_selection_values()
}
m_right_comboBox_nozzle_dia->SetSelection(NOZZLE_LIST_DEFAULT);
m_right_comboBox_nozzle_volume->Clear();
const ConfigOptionDef *nozzle_volume_type_def = print_config_def.get("nozzle_volume_type");
if (nozzle_volume_type_def && nozzle_volume_type_def->enum_keys_map) {
for (auto item : nozzle_volume_type_def->enum_labels) {
m_right_comboBox_nozzle_volume->AppendString(_L(item));
}
}
m_right_comboBox_nozzle_volume->SetSelection(int(NozzleVolumeType::nvtStandard));
fill_nozzle_volume_combo(m_right_comboBox_nozzle_volume);
}
}
@@ -980,13 +979,13 @@ NozzleVolumeType CalibrationPresetPage::get_nozzle_volume_type(int extruder_id)
if (curr_obj) {
if (curr_obj->is_multi_extruders()) {
if (extruder_id == LEFT_EXTRUDER_ID) {
return NozzleVolumeType(m_left_comboBox_nozzle_volume->GetSelection());
return NozzleVolumeType(intptr_t(m_left_comboBox_nozzle_volume->GetClientData(m_left_comboBox_nozzle_volume->GetSelection())));
} else if (extruder_id == RIGHT_EXTRUDER_ID) {
return NozzleVolumeType(m_right_comboBox_nozzle_volume->GetSelection());
return NozzleVolumeType(intptr_t(m_right_comboBox_nozzle_volume->GetClientData(m_right_comboBox_nozzle_volume->GetSelection())));
}
}
else
return NozzleVolumeType(m_comboBox_nozzle_volume->GetSelection());
return NozzleVolumeType(intptr_t(m_comboBox_nozzle_volume->GetClientData(m_comboBox_nozzle_volume->GetSelection())));
}
return NozzleVolumeType::nvtStandard;
}
@@ -2139,7 +2138,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj)
}
if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) {
m_left_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))));
select_nozzle_volume(m_left_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)));
} else {
m_left_comboBox_nozzle_volume->SetSelection(0);
}
@@ -2159,7 +2158,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj)
}
if (obj->GetExtderSystem()->GetNozzleFlowType(i) != NozzleFlowType::NONE_FLOWTYPE) {
m_right_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i))));
select_nozzle_volume(m_right_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(i)));
} else {
m_right_comboBox_nozzle_volume->SetSelection(0);
}
@@ -2197,7 +2196,7 @@ void CalibrationPresetPage::init_with_machine(MachineObject* obj)
else {
if ((obj->GetExtderSystem()->GetTotalExtderCount() > 0) && (obj->GetExtderSystem()->GetNozzleFlowType(0) != NozzleFlowType::NONE_FLOWTYPE))
{
m_comboBox_nozzle_volume->SetSelection(int(DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0))));
select_nozzle_volume(m_comboBox_nozzle_volume, DevNozzle::ToNozzleVolumeType(obj->GetExtderSystem()->GetNozzleFlowType(0)));
} else {
m_comboBox_nozzle_volume->SetSelection(0);
}
@@ -732,6 +732,7 @@ void CaliPASaveAutoPanel::sync_cali_result_for_multi_extruder(const std::vector<
case NozzleVolumeType::nvtStandard: nozzle_id_str += _L("Standard Flow"); break;
case NozzleVolumeType::nvtHighFlow: nozzle_id_str += _L("High Flow"); break;
case NozzleVolumeType::nvtTPUHighFlow: nozzle_id_str += _L("TPU High Flow"); break;
case NozzleVolumeType::nvtE3DHighFlow: nozzle_id_str += _L("E3D High Flow"); break;
default: break;
}
nozzle_id_value->SetLabel(nozzle_id_str);
+2
View File
@@ -1046,6 +1046,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
// Dropping the sparse layers outright leaves nothing to combine, so the two are exclusive.
toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !config->opt_bool("wipe_tower_no_sparse_layers"));
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
+1 -1
View File
@@ -2710,7 +2710,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent)
SetSizerAndFit(vsizer);
// We can now enable scrolling on hscroll
p->hscroll->SetScrollRate(30, 30);
p->hscroll->SetScrollRate(30, FromDIP(20));
on_window_geometry(this, [this]() {
p->init_dialog_size();
+5 -5
View File
@@ -699,7 +699,7 @@ CreateFilamentPresetDialog::CreateFilamentPresetDialog(wxWindow *parent)
m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY);
m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350)));
m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE);
m_scrolled_preset_panel->SetScrollRate(5, 5);
m_scrolled_preset_panel->SetScrollRate(5, FromDIP(20));
m_scrolled_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolled_sizer->Add(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
m_scrolled_sizer->Add(0, 0, 0, wxTOP, FromDIP(5));
@@ -1593,7 +1593,7 @@ CreatePrinterPresetDialog::CreatePrinterPresetDialog(wxWindow *parent)
m_page1 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_page1->SetBackgroundColour(*wxWHITE);
m_page1->SetScrollRate(5, 5);
m_page1->SetScrollRate(5, FromDIP(20));
m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE);
create_printer_page1(m_page1);
@@ -2652,7 +2652,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_presets_template_item(wxWindow *pa
wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolled_preset_window = new wxScrolledWindow(parent);
m_scrolled_preset_window->SetScrollRate(5, 5);
m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
//m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1)));
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(1500), FromDIP(-1)));
@@ -4297,7 +4297,7 @@ wxBoxSizer *ExportConfigsDialog::create_select_printer(wxWindow *parent)
optionSizer->SetMinSize(OPTION_SIZE);
horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10));
m_scrolled_preset_window = new wxScrolledWindow(parent);
m_scrolled_preset_window->SetScrollRate(5, 5);
m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400)));
m_scrolled_preset_window->SetSize(wxSize(FromDIP(660), FromDIP(400)));
@@ -4736,7 +4736,7 @@ wxBoxSizer *EditFilamentPresetDialog::create_preset_tree_sizer()
{
wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL);
m_preset_tree_window = new wxScrolledWindow(this);
m_preset_tree_window->SetScrollRate(5, 5);
m_preset_tree_window->SetScrollRate(5, FromDIP(20));
m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR);
m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400)));
m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300)));
+2 -1
View File
@@ -36,7 +36,8 @@ static NozzleVolumeType convert_to_nozzle_type(const std::string &str)
return NozzleVolumeType::nvtHighFlow;
else if (str[1] == 'U')
return NozzleVolumeType::nvtTPUHighFlow;
// Orca: no nvtE3DHighFlow in Orca's NozzleVolumeType; map 'B' to Standard
else if (str[1] == 'B')
return NozzleVolumeType::nvtE3DHighFlow;
else
return NozzleVolumeType::nvtStandard;
}
+25 -2
View File
@@ -10,6 +10,7 @@
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Time.hpp"
@@ -355,7 +356,10 @@ namespace Slic3r
obj->bind_state = "free";
obj->last_alive = Slic3r::Utils::get_current_time_utc();
obj->m_is_online = true;
// Route through set_online_state() (rather than writing m_is_online directly) so the
// DeviceOnline lifecycle event fires consistently; same effective value/behavior
// here since the object was already online in the common case.
obj->set_online_state(true);
obj->set_dev_name(dev_name);
/* if (!obj->dev_ip.empty()) {
Slic3r::GUI::wxGetApp().app_config->set_str("ip_address", obj->dev_id, obj->dev_ip);
@@ -373,6 +377,10 @@ namespace Slic3r
obj->bind_sec_link = sec_link;
obj->dev_connection_name = connection_name;
obj->bind_ssdp_version = ssdp_version;
// Discovery establishes the initial reachability state. Do not report it as an
// online transition; DeviceDiscovered below is the lifecycle event for a new
// device. Subsequent updates route through set_online_state(), so a known device
// still emits DeviceOnline/DeviceOffline when its reachability actually changes.
obj->m_is_online = true;
//load access code
@@ -389,6 +397,15 @@ namespace Slic3r
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " New Machine, dev_id= " << dev_id
<< ", ip = " << dev_ip <<", printer_name = " << dev_name
<< ", con_type= " << connect_type <<", signal= " << printer_signal << ", bind_state= " << bind_state;
// First discovery of a genuinely new device (not a periodic SSDP/heartbeat update to
// an already-known one, which is handled in the branch above).
{
LifecycleEventContext ctx;
ctx.name = dev_id;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceDiscovered, ctx);
}
}
update_local_machine(*obj);
}
@@ -984,8 +1001,14 @@ namespace Slic3r
}
void DeviceManager::OnSelectedMachineChanged(const std::string& /*pre_dev_id*/,
const std::string& /*new_dev_id*/)
const std::string& new_dev_id)
{
{
LifecycleEventContext ctx;
ctx.name = new_dev_id; // empty string is a valid deselection
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::DeviceSelected, ctx);
}
if (MachineObject* obj_ = get_selected_machine()) {
GUI::wxGetApp().sidebar().update_sync_status(obj_);
if(m_agent->get_filament_sync_mode() == FilamentSyncMode::subscription)
@@ -13,7 +13,7 @@
namespace Slic3r
{
// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow) ----------------------
// ---- DevNozzle: flow/volume conversions (Standard / High Flow / TPU High Flow / E3D High Flow) ------
// Device-reported U_FLOW nozzles map to nvtTPUHighFlow so a synced TPU-HF rack activates the H2C
// change_filament_gcode TPU-kit branch. nvtHybrid is a slicer-only sentinel with no device
// representation, so ToNozzleFlowType(nvtHybrid) falls through to NONE_FLOWTYPE.
@@ -24,6 +24,7 @@ NozzleFlowType DevNozzle::ToNozzleFlowType(const NozzleVolumeType& type)
case NozzleVolumeType::nvtStandard: return NozzleFlowType::S_FLOW;
case NozzleVolumeType::nvtHighFlow: return NozzleFlowType::H_FLOW;
case NozzleVolumeType::nvtTPUHighFlow: return NozzleFlowType::U_FLOW;
case NozzleVolumeType::nvtE3DHighFlow: return NozzleFlowType::E_FLOW;
default: return NozzleFlowType::NONE_FLOWTYPE;
}
}
@@ -34,6 +35,7 @@ NozzleVolumeType DevNozzle::ToNozzleVolumeType(const NozzleFlowType& type)
case NozzleFlowType::S_FLOW: return NozzleVolumeType::nvtStandard;
case NozzleFlowType::H_FLOW: return NozzleVolumeType::nvtHighFlow;
case NozzleFlowType::U_FLOW: return NozzleVolumeType::nvtTPUHighFlow;
case NozzleFlowType::E_FLOW: return NozzleVolumeType::nvtE3DHighFlow;
default: {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << "nozzle flow type None convert to nozzle volume type Standard";
return NozzleVolumeType::nvtStandard;
@@ -47,6 +49,7 @@ wxString DevNozzle::GetNozzleFlowTypeStr(NozzleFlowType type)
case NozzleFlowType::H_FLOW: return _L("High Flow");
case NozzleFlowType::S_FLOW: return _L("Standard");
case NozzleFlowType::U_FLOW: return _L("TPU High Flow");
case NozzleFlowType::E_FLOW: return _L("E3D High Flow");
default: break;
}
@@ -61,6 +64,7 @@ std::string DevNozzle::GetNozzleFlowTypeString(NozzleFlowType type)
case NozzleFlowType::H_FLOW: return "High Flow";
case NozzleFlowType::S_FLOW: return "Standard";
case NozzleFlowType::U_FLOW: return "TPU High Flow";
case NozzleFlowType::E_FLOW: return "E3D High Flow";
default: return "Unknown";
}
}
@@ -73,6 +77,7 @@ std::string DevNozzle::ToNozzleFlowString(const NozzleFlowType& type)
case NozzleFlowType::S_FLOW: return "Standard";
case NozzleFlowType::H_FLOW: return "High Flow";
case NozzleFlowType::U_FLOW: return "TPU High Flow";
case NozzleFlowType::E_FLOW: return "E3D High Flow";
default: return std::string();
}
}
@@ -408,6 +413,7 @@ static unordered_map<string, NozzleFlowType> _str2_nozzle_flow_type = {
{"X", NozzleFlowType::S_FLOW},
{"E", NozzleFlowType::H_FLOW}, // E3D high-flow
{"U", NozzleFlowType::U_FLOW}, // TPU 1.75 high-flow -> nvtTPUHighFlow
{"B", NozzleFlowType::E_FLOW}, // E3D High Flow -> nvtE3DHighFlow
};
static unordered_map<string, NozzleType> _str2_nozzle_type = {

Some files were not shown because too many files have changed in this diff Show More