Merge branch 'main' into feat/plugin-feature

This commit is contained in:
SoftFever
2026-07-15 17:19:14 +08:00
7 changed files with 107 additions and 45 deletions

View File

@@ -212,14 +212,26 @@ void install_slice_context_log_sink()
// -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. 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)
// 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.
int slice_all_printers(const std::string &vendor, const std::string &outdir)
{
install_slice_context_log_sink();
if (!outdir.empty()) {
boost::system::error_code ec;
fs::create_directories(outdir, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << "Could not create output directory \"" << outdir << "\": " << ec.message();
std::cout << "Validation failed" << std::endl;
return 1;
}
std::cout << "Saving sliced g-code to " << outdir << std::endl;
}
PresetBundle bundle;
bundle.set_is_validation_mode(true);
bundle.set_vendor_to_validate(vendor); // empty == all vendors
@@ -323,6 +335,10 @@ int slice_all_printers(const std::string &vendor)
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;
@@ -364,6 +380,7 @@ int main(int argc, char* argv[])
("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.")
("check_filament_subtypes,f", po::bool_switch()->default_value(false), "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
@@ -389,6 +406,7 @@ int main(int argc, char* argv[])
int log_level = vm["log_level"].as<int>();
bool generate_user_preset = vm["generate_presets"].as<bool>();
bool slice_mode = vm["slice"].as<bool>();
std::string slice_outdir = vm["outdir"].as<std::string>();
bool check_filament_subtypes = vm["check_filament_subtypes"].as<bool>();
// check if path is valid, and return error if not
@@ -419,7 +437,7 @@ int main(int argc, char* argv[])
// Slice mode expands every printer's custom g-code by actually slicing (see slice_all_printers).
// A distinct opt-in mode so the default static checks stay fast for every profile PR.
if (slice_mode)
return slice_all_printers(vendor);
return slice_all_printers(vendor, slice_outdir);
auto preset_bundle = new PresetBundle();
// preset_bundle->setup_directories();

View File

@@ -352,8 +352,7 @@ namespace Slic3r
int k,
const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits,
int* cost,
int timeout_ms)
int* cost)
{
auto distance_evaluator = std::make_shared<FlushDistanceEvaluator>(ctx.model_info.flush_matrix, used_filaments, ctx.model_info.layer_filaments);
KMediods PAM(k, (int)used_filaments.size(), distance_evaluator, ctx.machine_info.master_extruder_id);
@@ -369,7 +368,7 @@ namespace Slic3r
}
PAM.set_cluster_group_size(cluster_size_limit);
PAM.do_clustering(ctx, timeout_ms, 30);
PAM.do_clustering(ctx, m_clustering_budget);
m_memoryed_heap = PAM.get_memoryed_groups();
@@ -793,7 +792,7 @@ namespace Slic3r
2.1 In each cluster, make the point that minimizes the sum of distances within the cluster the medoid
2.2 Reassign each point to the cluster defined by the closest medoid determined in the previous step
*/
void KMediods::do_clustering(const FilamentGroupContext &context, int timeout_ms, int retry)
void KMediods::do_clustering(const FilamentGroupContext& context, const ClusteringBudget& budget)
{
FlushTimeMachine T;
T.time_machine_start();
@@ -817,7 +816,11 @@ namespace Slic3r
double best_cluster_cost = std::numeric_limits<double>::max();
int retry_count = 0;
while (retry_count < retry && T.time_machine_end() < timeout_ms) {
// Run at least one restart; otherwise every filament would stay in the default group.
const int retry = std::max(1, budget.max_restarts);
auto within_budget = [&]() { return budget.timeout_ms <= 0 || T.time_machine_end() < budget.timeout_ms; };
while (retry_count < retry && within_budget()) {
std::vector<int> curr_cluster_centers = init_cluster_center(m_placeable_limits, m_unplaceable_limits, m_max_cluster_size, m_cluster_group_size, retry_count);
std::vector<int> curr_cluster_labels = assign_cluster_label(curr_cluster_centers, m_placeable_limits, m_unplaceable_limits, m_max_cluster_size, m_cluster_group_size);
double curr_cluster_cost = evaluate_labels(curr_cluster_labels);
@@ -826,7 +829,7 @@ namespace Slic3r
update_memoryed_groups(g, memory_threshold, memoryed_groups);
bool mediods_changed = true;
while (mediods_changed && T.time_machine_end() < timeout_ms) {
while (mediods_changed && within_budget()) {
mediods_changed = false;
double best_swap_cost = curr_cluster_cost;
int best_swap_cluster = -1;
@@ -889,7 +892,7 @@ namespace Slic3r
if (estimated < ENUM_THRESHOLD)
result = calc_group_by_enum(k, used_filaments, unplaceable_limits, cost);
else
result = calc_group_by_kmedoids(k, used_filaments, unplaceable_limits, cost, 3000);
result = calc_group_by_kmedoids(k, used_filaments, unplaceable_limits, cost);
change_memoryed_heaps_to_arrays(m_memoryed_heap, ctx.group_info.total_filament_num, used_filaments, m_memoryed_groups);

View File

@@ -142,6 +142,16 @@ namespace Slic3r
FilamentGroupContext::SpeedInfo m_speed_info;
};
// Search budget for the k-medoids clustering, an anytime search. Each restart is seeded from its
// own index, so what it returns depends on how many restarts complete before the clock expires,
// and therefore on the speed of the machine. A timeout_ms <= 0 removes the clock and bounds the
// search by max_restarts alone.
struct ClusteringBudget
{
int timeout_ms = 3000;
int max_restarts = 30;
};
class FilamentGroup
{
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
@@ -149,6 +159,8 @@ namespace Slic3r
public:
explicit FilamentGroup(const FilamentGroupContext& ctx_) :ctx(ctx_) {}
public:
void set_clustering_budget(const ClusteringBudget& budget) { m_clustering_budget = budget; }
std::vector<int> calc_filament_group(int * cost = nullptr);
std::vector<std::vector<int>> get_memoryed_groups()const { return m_memoryed_groups; }
@@ -162,7 +174,7 @@ namespace Slic3r
std::vector<int> calc_group_by_enum(int k, const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
std::vector<int> calc_group_by_kmedoids(int k, const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr, int timeout_ms = 500);
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
std::map<int, int> rebuild_unprintables(const std::vector<unsigned int>& used_filaments, const std::map<int,int>& extruder_unprintables);
std::unordered_map<int, std::vector<int>> rebuild_nozzle_unprintables(const std::vector<unsigned int>& used_filaments, const std::unordered_map<int, std::vector<int>>& extruder_unprintables, const std::vector<int>& filament_volume_map);
@@ -175,6 +187,7 @@ namespace Slic3r
FilamentGroupContext ctx;
MemoryedGroupHeap m_memoryed_heap;
std::vector<std::vector<int>> m_memoryed_groups;
ClusteringBudget m_clustering_budget;
public:
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq;
};
@@ -220,7 +233,7 @@ namespace Slic3r
void set_memory_threshold(double threshold) { memory_threshold = threshold; }
MemoryedGroupHeap get_memoryed_groups()const { return memoryed_groups; }
void do_clustering(const FilamentGroupContext& context, int timeout_ms = 100, int retry = 10);
void do_clustering(const FilamentGroupContext& context, const ClusteringBudget& budget);
std::vector<int> get_cluster_labels()const { return m_cluster_labels; }
protected: