mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
BBL Port Color Mix Base
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
#include "GCode/ToolOrderUtils.hpp"
|
||||
#include "FilamentGroupUtils.hpp"
|
||||
#include "MultiNozzleUtils.hpp"
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
@@ -22,8 +24,13 @@
|
||||
#endif
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <libslic3r.h>
|
||||
@@ -402,7 +409,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
|
||||
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
|
||||
// so we shouldn't reorder first layer in reorder function
|
||||
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
|
||||
this->resolve_mixed_filaments(print.config());
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
this->enforce_mixed_component_order();
|
||||
m_sorted = true;
|
||||
|
||||
double max_layer_height = 0.;
|
||||
@@ -422,6 +431,9 @@ void ToolOrdering::sort_and_build_data(const Print& print, unsigned int first_ex
|
||||
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
|
||||
if (this->insert_wipe_tower_extruder()) {
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
// Orca reorders a second time here (BBS has no such path); re-enforce so the
|
||||
// mixed sub-layer component order survives the extra pass.
|
||||
this->enforce_mixed_component_order();
|
||||
this->fill_wipe_tower_partitions(print.config(), object_bottom_z, max_layer_height);
|
||||
}
|
||||
|
||||
@@ -433,7 +445,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
|
||||
// if first extruder is -1, we can decide the first layer tool order before doing reorder function
|
||||
// so we shouldn't reorder first layer in reorder function
|
||||
bool reorder_first_layer = (first_extruder != (unsigned int)(-1));
|
||||
this->resolve_mixed_filaments(object.print()->config());
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
this->enforce_mixed_component_order();
|
||||
m_sorted = true;
|
||||
|
||||
double max_layer_height = calc_max_layer_height(object.print()->config(), object.config().layer_height);
|
||||
@@ -441,6 +455,9 @@ void ToolOrdering::sort_and_build_data(const PrintObject& object , unsigned int
|
||||
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
|
||||
if (this->insert_wipe_tower_extruder()) {
|
||||
reorder_extruders_for_minimum_flush_volume(reorder_first_layer);
|
||||
// Orca reorders a second time here (BBS has no such path); re-enforce so the
|
||||
// mixed sub-layer component order survives the extra pass.
|
||||
this->enforce_mixed_component_order();
|
||||
this->fill_wipe_tower_partitions(object.print()->config(), object.layers().front()->print_z - object.layers().front()->height, max_layer_height);
|
||||
}
|
||||
|
||||
@@ -723,6 +740,38 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
it_per_layer_extruder_override = per_layer_extruder_switches.begin();
|
||||
unsigned int extruder_override = 0;
|
||||
|
||||
// Pre-compute 1-based IDs of mixed filament slots for per-object tracking.
|
||||
// mixed_slots_1based covers ALL mixed slots (needed by calc_slot_lh for
|
||||
// accurate layer height when a slot skips layers). gradient_slots_1based
|
||||
// and per_part_slots_1based are subsets for gradient-specific logic.
|
||||
std::set<unsigned int> mixed_slots_1based;
|
||||
std::set<unsigned int> gradient_slots_1based;
|
||||
std::set<unsigned int> per_part_slots_1based;
|
||||
{
|
||||
const PrintConfig &cfg = object.print()->config();
|
||||
const auto &is_mixed = cfg.filament_is_mixed.values;
|
||||
const auto &grad_flags = cfg.filament_mixed_gradient.values;
|
||||
const auto &per_part_flags = cfg.filament_mixed_gradient_per_part.values;
|
||||
const auto &comp_strs = cfg.filament_mixed_components.values;
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i])
|
||||
continue;
|
||||
auto comps = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
|
||||
if (comps.size() < 2)
|
||||
continue;
|
||||
mixed_slots_1based.insert(static_cast<unsigned int>(i + 1));
|
||||
// Gradient/per-part are only defined for 2-component slots; keep their
|
||||
// tracking limited to them (mirrors the is_gradient guard at resolve time).
|
||||
if (comps.size() != 2)
|
||||
continue;
|
||||
if (i >= grad_flags.size() || !grad_flags[i])
|
||||
continue;
|
||||
gradient_slots_1based.insert(static_cast<unsigned int>(i + 1));
|
||||
if (i < per_part_flags.size() && per_part_flags[i])
|
||||
per_part_slots_1based.insert(static_cast<unsigned int>(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: collect first layer extruders of an object's wall, which will be used by brim generator
|
||||
int layerCount = 0;
|
||||
std::vector<int> firstLayerExtruders;
|
||||
@@ -732,6 +781,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
for (auto layer : object.layers()) {
|
||||
LayerTools &layer_tools = this->tools_for_layer(layer->print_z);
|
||||
|
||||
m_object_all_layer_indices[&object].push_back(
|
||||
static_cast<size_t>(&layer_tools - m_layer_tools.data()));
|
||||
|
||||
// Override extruder with the next
|
||||
for (; it_per_layer_extruder_override != per_layer_extruder_switches.end() && it_per_layer_extruder_override->first < layer->print_z + EPSILON; ++ it_per_layer_extruder_override)
|
||||
extruder_override = (int)it_per_layer_extruder_override->second;
|
||||
@@ -739,6 +791,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
// Store the current extruder override (set to zero if no overriden), so that layer_tools.wiping_extrusions().is_overridable_and_mark() will use it.
|
||||
layer_tools.extruder_override = extruder_override;
|
||||
|
||||
// Snapshot extruders before this object's regions to track new additions.
|
||||
const size_t ext_snapshot = layer_tools.extruders.size();
|
||||
|
||||
// What extruders are required to print this object layer?
|
||||
for (const LayerRegion *layerm : layer->regions()) {
|
||||
const PrintRegion ®ion = layerm->region();
|
||||
@@ -805,6 +860,54 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
if (has_internal_solid || has_top_solid_surface || has_bottom_surface || has_infill)
|
||||
layer_tools.has_object = true;
|
||||
}
|
||||
|
||||
// Record mixed slot usage for this object at this layer.
|
||||
// All mixed slots are tracked (not just gradient) so that calc_slot_lh
|
||||
// can compute accurate layer heights even when a slot skips layers.
|
||||
if (!mixed_slots_1based.empty()) {
|
||||
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
|
||||
std::set<unsigned int> seen;
|
||||
for (size_t ei = ext_snapshot; ei < layer_tools.extruders.size(); ++ei) {
|
||||
unsigned int ext_1based = layer_tools.extruders[ei];
|
||||
if (mixed_slots_1based.count(ext_1based) && seen.insert(ext_1based).second)
|
||||
m_mixed_object_layers[ext_1based - 1][&object].push_back(layer_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-part gradient: walk LayerRegions and record which (slot, ModelVolume) pairs
|
||||
// contributed to this layer. Only regions tagged by PrintApply.cpp's get_create_region
|
||||
// (i.e. gradient_volume_id().valid()) are considered, so this loop is a strict no-op
|
||||
// unless per_part_gradient is enabled for at least one slot AND the corresponding
|
||||
// ModelObject has >=2 model-part volumes using that slot. The per-object pass above is
|
||||
// unaffected — both run the same layer's data through orthogonal containers.
|
||||
if (!per_part_slots_1based.empty()) {
|
||||
size_t layer_idx = static_cast<size_t>(&layer_tools - m_layer_tools.data());
|
||||
std::set<std::pair<unsigned int, ObjectID>> vol_seen;
|
||||
for (const LayerRegion *layerm : layer->regions()) {
|
||||
if (layerm->slices.empty())
|
||||
continue;
|
||||
const PrintRegion ®ion = layerm->region();
|
||||
ObjectID vol_id = region.gradient_volume_id();
|
||||
if (! vol_id.valid())
|
||||
continue;
|
||||
const PrintRegionConfig &rcfg = region.config();
|
||||
// Orca splits BBS's three role slots into five; cover them all so a mixed
|
||||
// slot used by any role is tracked.
|
||||
const unsigned int role_slots[5] = {
|
||||
static_cast<unsigned int>(rcfg.outer_wall_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.inner_wall_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.sparse_infill_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.top_surface_filament_id.value),
|
||||
static_cast<unsigned int>(rcfg.bottom_surface_filament_id.value),
|
||||
};
|
||||
for (unsigned int ext_1based : role_slots) {
|
||||
if (ext_1based >= 1
|
||||
&& per_part_slots_1based.count(ext_1based)
|
||||
&& vol_seen.insert({ext_1based, vol_id}).second)
|
||||
m_gradient_volume_layers[ext_1based - 1][{&object, vol_id}].push_back(layer_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
layerCount++;
|
||||
}
|
||||
|
||||
@@ -1945,6 +2048,594 @@ MultiNozzleUtils::LayeredNozzleGroupResult ToolOrdering::build_sequential_group_
|
||||
return result ? *result : MultiNozzleUtils::LayeredNozzleGroupResult();
|
||||
}
|
||||
|
||||
static double snap_to_simple_fraction(double r, int max_denom = 10)
|
||||
{
|
||||
double best_r = r;
|
||||
double best_err = 1.0;
|
||||
for (int q = 1; q <= max_denom; ++q) {
|
||||
int p = (int)std::round(r * q);
|
||||
if (p < 0) p = 0;
|
||||
if (p > q) p = q;
|
||||
double candidate = (double)p / q;
|
||||
double err = std::abs(candidate - r);
|
||||
if (err < best_err) {
|
||||
best_err = err;
|
||||
best_r = candidate;
|
||||
}
|
||||
}
|
||||
return best_r;
|
||||
}
|
||||
|
||||
void ToolOrdering::resolve_mixed_filaments(const PrintConfig &config)
|
||||
{
|
||||
const auto &is_mixed = config.filament_is_mixed.values;
|
||||
const auto &comp_strs = config.filament_mixed_components.values;
|
||||
const auto &ratio_strs = config.filament_mixed_sublayer_ratios.values;
|
||||
|
||||
if (!has_any_mixed_filament(is_mixed))
|
||||
return;
|
||||
|
||||
const bool sublayer_enabled = config.enable_mixed_color_sublayer.value;
|
||||
|
||||
struct SlotInfo {
|
||||
std::vector<unsigned int> components; // 1-based
|
||||
std::vector<double> ratios;
|
||||
std::vector<long long> accum; // deficit accumulator (integer, unit: 1e-6 mm)
|
||||
};
|
||||
std::vector<SlotInfo> slots(is_mixed.size());
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i])
|
||||
continue;
|
||||
slots[i].components = parse_mixed_components(i < comp_strs.size() ? comp_strs[i] : "");
|
||||
if (slots[i].components.size() < 2) {
|
||||
slots[i].components.clear();
|
||||
continue;
|
||||
}
|
||||
for (unsigned int cid : slots[i].components) {
|
||||
unsigned int idx0 = cid - 1;
|
||||
if (idx0 >= is_mixed.size() || (idx0 < is_mixed.size() && is_mixed[idx0])) {
|
||||
slots[i].components.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slots[i].components.empty())
|
||||
continue;
|
||||
slots[i].ratios = parse_mixed_ratios(
|
||||
i < ratio_strs.size() ? ratio_strs[i] : "", slots[i].components.size());
|
||||
if (!sublayer_enabled) {
|
||||
for (double &r : slots[i].ratios)
|
||||
r = snap_to_simple_fraction(r);
|
||||
double sum = 0;
|
||||
for (double r : slots[i].ratios) sum += r;
|
||||
if (sum > 0)
|
||||
for (double &r : slots[i].ratios) r /= sum;
|
||||
}
|
||||
slots[i].accum.assign(slots[i].components.size(), 0LL);
|
||||
}
|
||||
|
||||
// Parse gradient settings per slot
|
||||
const auto &gradient_flags = config.filament_mixed_gradient.values;
|
||||
const auto &gradient_range_strs = config.filament_mixed_gradient_range.values;
|
||||
const auto &gradient_curve_strs = config.filament_mixed_gradient_curve.values;
|
||||
struct GradientInfo {
|
||||
double start = 0.10;
|
||||
double end_val = 0.90;
|
||||
GradientCurve curve; // empty -> use linear (start, end_val); non-empty wins
|
||||
};
|
||||
std::vector<bool> is_gradient(is_mixed.size(), false);
|
||||
std::vector<GradientInfo> gradient_info(is_mixed.size());
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i) {
|
||||
if (!is_mixed[i] || slots[i].components.size() != 2)
|
||||
continue;
|
||||
if (i >= gradient_flags.size() || !gradient_flags[i])
|
||||
continue;
|
||||
is_gradient[i] = true;
|
||||
if (i < gradient_range_strs.size() && !gradient_range_strs[i].empty()) {
|
||||
CNumericLocalesSetter c_locale_setter;
|
||||
float v0 = 0, v1 = 0;
|
||||
if (std::sscanf(gradient_range_strs[i].c_str(), "%f,%f", &v0, &v1) == 2 &&
|
||||
v0 > 0 && v0 < 1.0 && v1 > 0 && v1 < 1.0) {
|
||||
gradient_info[i].start = v0;
|
||||
gradient_info[i].end_val = v1;
|
||||
}
|
||||
}
|
||||
if (i < gradient_curve_strs.size() && !gradient_curve_strs[i].empty())
|
||||
gradient_info[i].curve = parse_gradient_curve(gradient_curve_strs[i]);
|
||||
}
|
||||
|
||||
// Pass 1: identify continuous runs for each gradient slot (Per-Run).
|
||||
// A "run" is a maximal sequence of consecutive layers where the slot appears.
|
||||
struct GradientRunInfo {
|
||||
std::vector<size_t> run_lengths;
|
||||
int current_run = -1;
|
||||
size_t current_idx = 0;
|
||||
bool prev_appeared = false;
|
||||
bool last_absent_was_relevant = false;
|
||||
};
|
||||
std::map<unsigned int, GradientRunInfo> gradient_runs;
|
||||
for (size_t i = 0; i < is_mixed.size(); ++i)
|
||||
if (is_gradient[i]) gradient_runs[static_cast<unsigned int>(i)] = {};
|
||||
|
||||
// Build per-slot sets of all layer indices where any slot-owning object has a
|
||||
// layer. Used by gradient run detection (a gap is real only if the slot is
|
||||
// absent at a layer belonging to one of its own objects) and by calc_slot_lh
|
||||
// to keep prev_relevant_z_for_slot current even when a slot skips many layers.
|
||||
std::map<unsigned int, std::set<size_t>> slot_relevant_layers;
|
||||
for (auto &[slot_idx, obj_map] : m_mixed_object_layers) {
|
||||
for (auto &[obj, _] : obj_map) {
|
||||
auto it = m_object_all_layer_indices.find(obj);
|
||||
if (it != m_object_all_layer_indices.end())
|
||||
slot_relevant_layers[slot_idx].insert(it->second.begin(), it->second.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (!gradient_runs.empty()) {
|
||||
for (size_t li = 0; li < m_layer_tools.size(); ++li) {
|
||||
if (li == 0) continue;
|
||||
const auto < = m_layer_tools[li];
|
||||
for (auto &[slot, run] : gradient_runs) {
|
||||
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
|
||||
if (here) {
|
||||
bool real_gap = false;
|
||||
if (!run.prev_appeared && !run.run_lengths.empty()) {
|
||||
real_gap = run.last_absent_was_relevant;
|
||||
}
|
||||
if (run.run_lengths.empty() || real_gap)
|
||||
run.run_lengths.push_back(0);
|
||||
run.run_lengths.back()++;
|
||||
run.last_absent_was_relevant = false;
|
||||
} else if (!run.run_lengths.empty()) {
|
||||
auto rel_it = slot_relevant_layers.find(slot);
|
||||
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(li))
|
||||
run.last_absent_was_relevant = true;
|
||||
}
|
||||
run.prev_appeared = here;
|
||||
}
|
||||
}
|
||||
for (auto &[slot, run] : gradient_runs) {
|
||||
run.current_run = -1;
|
||||
run.current_idx = 0;
|
||||
run.prev_appeared = false;
|
||||
run.last_absent_was_relevant = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-object gradient: pre-compute per-object runs (respecting Z gaps within each object).
|
||||
struct PerObjRunState {
|
||||
std::vector<size_t> run_start_offsets; // index into layer_indices where each run starts
|
||||
std::vector<size_t> run_lengths;
|
||||
int current_run = -1;
|
||||
size_t current_idx = 0;
|
||||
};
|
||||
|
||||
// Detect whether a gap between two consecutive gradient-slot appearances is a
|
||||
// real run break. A gap is real only if the object has its own layer inside the
|
||||
// gap that does NOT use the gradient slot (i.e. the slot was genuinely absent).
|
||||
// Uses lower_bound to skip global indices that don't belong to the object.
|
||||
auto has_real_gap = [](size_t prev_idx, size_t cur_idx,
|
||||
const std::set<size_t>& obj_set,
|
||||
const std::set<size_t>& slot_set) -> bool {
|
||||
for (auto it = obj_set.lower_bound(prev_idx + 1);
|
||||
it != obj_set.end() && *it < cur_idx; ++it) {
|
||||
if (!slot_set.count(*it))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Segment a sorted list of layer indices into runs, using has_real_gap to decide
|
||||
// where to break. Shared by the per-object and per-volume paths below.
|
||||
auto segment_runs = [&](const std::vector<size_t>& layer_indices,
|
||||
const std::set<size_t>& obj_set,
|
||||
const std::set<size_t>& slot_set) -> PerObjRunState {
|
||||
PerObjRunState st;
|
||||
for (size_t i = 0; i < layer_indices.size(); ++i) {
|
||||
bool new_run = (i == 0) ||
|
||||
has_real_gap(layer_indices[i - 1], layer_indices[i], obj_set, slot_set);
|
||||
if (new_run) {
|
||||
st.run_start_offsets.push_back(i);
|
||||
st.run_lengths.push_back(0);
|
||||
}
|
||||
st.run_lengths.back()++;
|
||||
}
|
||||
return st;
|
||||
};
|
||||
|
||||
std::map<unsigned int, std::map<const PrintObject*, PerObjRunState>> per_obj_runs;
|
||||
for (auto &[slot, obj_map] : m_mixed_object_layers) {
|
||||
if (slot >= is_gradient.size() || !is_gradient[slot])
|
||||
continue;
|
||||
for (auto &[obj, layer_indices] : obj_map) {
|
||||
sort_remove_duplicates(layer_indices);
|
||||
// Erase layer 0 — this mutation is also relied upon by the Pass 2 binary_search below.
|
||||
if (!layer_indices.empty() && layer_indices.front() == 0)
|
||||
layer_indices.erase(layer_indices.begin());
|
||||
|
||||
const auto &all_obj_layers = m_object_all_layer_indices[obj];
|
||||
std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end());
|
||||
std::set<size_t> grad_set(layer_indices.begin(), layer_indices.end());
|
||||
|
||||
per_obj_runs[slot][obj] = segment_runs(layer_indices, all_obj_set, grad_set);
|
||||
}
|
||||
}
|
||||
|
||||
// Per-volume gradient: mirror the per-object run-segmentation logic above for
|
||||
// m_gradient_volume_layers. When per_part_gradient is off (or no qualifying volume exists),
|
||||
// m_gradient_volume_layers is empty and per_vol_runs ends up empty too — so all subsequent
|
||||
// checks of `per_vol_runs.find(slot) != end()` will fail and the legacy per-object path
|
||||
// remains the only path taken.
|
||||
using VolumeKey = LayerTools::MixedSubLayerGroup::VolumeKey;
|
||||
std::map<unsigned int, std::map<VolumeKey, PerObjRunState>> per_vol_runs;
|
||||
for (auto &[slot, vol_map] : m_gradient_volume_layers) {
|
||||
if (slot >= is_gradient.size() || !is_gradient[slot])
|
||||
continue;
|
||||
for (auto &[vkey, layer_indices] : vol_map) {
|
||||
sort_remove_duplicates(layer_indices);
|
||||
if (!layer_indices.empty() && layer_indices.front() == 0)
|
||||
layer_indices.erase(layer_indices.begin());
|
||||
|
||||
const auto &all_obj_layers = m_object_all_layer_indices[vkey.obj];
|
||||
std::set<size_t> all_obj_set(all_obj_layers.begin(), all_obj_layers.end());
|
||||
std::set<size_t> vol_grad_set(layer_indices.begin(), layer_indices.end());
|
||||
|
||||
per_vol_runs[slot][vkey] = segment_runs(layer_indices, all_obj_set, vol_grad_set);
|
||||
}
|
||||
}
|
||||
// Pass 2: resolve per layer
|
||||
coordf_t prev_print_z = 0.;
|
||||
// Track last print_z per mixed slot so that layer height is computed from the
|
||||
// slot's own previous appearance, not from a global Z that may include layers
|
||||
// belonging only to other objects with different layer heights.
|
||||
std::map<unsigned int, coordf_t> prev_print_z_for_slot;
|
||||
// Track the last Z where a slot-owning object had ANY layer (regardless of
|
||||
// whether the slot was present). Used to detect genuine gaps: if the slot was
|
||||
// absent but its owner objects had layers, prev_relevant_z advances while
|
||||
// prev_print_z_for_slot stays stale. Taking the max of both gives correct lh.
|
||||
std::map<unsigned int, coordf_t> prev_relevant_z_for_slot;
|
||||
|
||||
// Compute the effective layer height for a mixed slot by choosing the best
|
||||
// reference Z among: (1) the slot's own last Z, (2) the last Z where the
|
||||
// slot's owning object had any layer, (3) the global previous Z as fallback
|
||||
// when the slot appears for the first time.
|
||||
auto calc_slot_lh = [&](unsigned int ext, coordf_t print_z) -> double {
|
||||
auto slot_pz_it = prev_print_z_for_slot.find(ext);
|
||||
auto rel_pz_it = prev_relevant_z_for_slot.find(ext);
|
||||
coordf_t base_z = prev_print_z;
|
||||
if (slot_pz_it != prev_print_z_for_slot.end()) {
|
||||
base_z = slot_pz_it->second;
|
||||
if (rel_pz_it != prev_relevant_z_for_slot.end())
|
||||
base_z = std::max(base_z, rel_pz_it->second);
|
||||
}
|
||||
double lh = print_z - base_z;
|
||||
return (lh > 0.) ? lh : 0.2; // 0.2mm safety fallback; should not trigger in normal operation
|
||||
};
|
||||
|
||||
for (LayerTools < : m_layer_tools) {
|
||||
size_t layer_idx = static_cast<size_t>(< - m_layer_tools.data());
|
||||
|
||||
// Update gradient run state (skip first layer to match counting).
|
||||
if (layer_idx > 0) {
|
||||
for (auto &[slot, run] : gradient_runs) {
|
||||
bool here = std::find(lt.extruders.begin(), lt.extruders.end(), slot) != lt.extruders.end();
|
||||
if (here) {
|
||||
if (!run.prev_appeared) {
|
||||
if (run.last_absent_was_relevant || run.current_run < 0) {
|
||||
run.current_run++;
|
||||
run.current_idx = 0;
|
||||
}
|
||||
}
|
||||
run.last_absent_was_relevant = false;
|
||||
} else {
|
||||
auto rel_it = slot_relevant_layers.find(slot);
|
||||
if (rel_it != slot_relevant_layers.end() && rel_it->second.count(layer_idx))
|
||||
run.last_absent_was_relevant = true;
|
||||
}
|
||||
run.prev_appeared = here;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned int> new_extruders;
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
if (ext >= slots.size() || slots[ext].components.empty()) {
|
||||
new_extruders.push_back(ext);
|
||||
continue;
|
||||
}
|
||||
auto &s = slots[ext];
|
||||
|
||||
// Skip sublayer splitting for the first layer to preserve bed adhesion.
|
||||
if (sublayer_enabled && layer_idx > 0) {
|
||||
double lh = calc_slot_lh(ext, lt.print_z);
|
||||
size_t n = s.components.size();
|
||||
|
||||
std::vector<double> sub_heights;
|
||||
bool gradient_last_no_split = false;
|
||||
unsigned int gradient_last_dominant_0b = 0;
|
||||
if (is_gradient[ext] && n == 2) {
|
||||
auto gr_it = gradient_runs.find(ext);
|
||||
if (gr_it != gradient_runs.end() && gr_it->second.current_run >= 0 &&
|
||||
static_cast<size_t>(gr_it->second.current_run) < gr_it->second.run_lengths.size()) {
|
||||
auto &run = gr_it->second;
|
||||
size_t N = run.run_lengths[run.current_run];
|
||||
size_t idx = run.current_idx++;
|
||||
double t = (N > 0) ? (2.0 * idx + 1.0) / (2.0 * N) : 0.5;
|
||||
// Custom curve wins over linear range when present; OFF path stays bit-identical.
|
||||
double r1 = gradient_info[ext].curve.empty()
|
||||
? (gradient_info[ext].start + (gradient_info[ext].end_val - gradient_info[ext].start) * t)
|
||||
: sample_gradient_curve(gradient_info[ext].curve, t);
|
||||
double r2 = 1.0 - r1;
|
||||
sub_heights.push_back(r1 * lh);
|
||||
sub_heights.push_back(r2 * lh);
|
||||
// The sublayer split path sorts components by physical ID ascending;
|
||||
// the higher-ID component ends up on top (visible surface). If the
|
||||
// gradient's dominant component has the lower physical ID, splitting
|
||||
// would put the non-dominant color on the visible top surface. In
|
||||
// that case, skip the split and print this final run-layer as pure
|
||||
// dominant color to preserve the gradient appearance.
|
||||
if (idx == N - 1) {
|
||||
// When r1 == r2 (exactly 50/50), component[0] is treated as dominant.
|
||||
size_t dominant = (r1 >= r2) ? 0 : 1;
|
||||
unsigned int dom_0b = s.components[dominant] - 1;
|
||||
unsigned int oth_0b = s.components[1 - dominant] - 1;
|
||||
if (dom_0b < oth_0b) {
|
||||
gradient_last_no_split = true;
|
||||
gradient_last_dominant_0b = dom_0b;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (double r : s.ratios)
|
||||
sub_heights.push_back(r * lh);
|
||||
}
|
||||
} else {
|
||||
for (double r : s.ratios)
|
||||
sub_heights.push_back(r * lh);
|
||||
}
|
||||
|
||||
// Per-part gradient: when this slot has any qualifying volume, the global
|
||||
// no-split short-circuit must NOT bypass MixedSubLayerGroup creation — each
|
||||
// volume needs its own no-split decision in GCode.cpp (a per-volume "last
|
||||
// run-layer" can occur on a different layer index than the per-object one). We
|
||||
// still keep the per-object short-circuit when per_vol_runs[ext] is empty, which
|
||||
// covers the legacy path bit-identically.
|
||||
bool per_vol_active_for_slot = per_vol_runs.find(ext) != per_vol_runs.end()
|
||||
&& !per_vol_runs[ext].empty();
|
||||
|
||||
if (gradient_last_no_split && !per_vol_active_for_slot) {
|
||||
lt.mixed_filament_resolution[ext] = gradient_last_dominant_0b;
|
||||
new_extruders.push_back(gradient_last_dominant_0b);
|
||||
prev_print_z_for_slot[ext] = lt.print_z;
|
||||
continue;
|
||||
}
|
||||
|
||||
LayerTools::MixedSubLayerGroup grp;
|
||||
grp.mixed_slot_0based = ext;
|
||||
grp.layer_height = lh;
|
||||
grp.is_gradient = is_gradient[ext];
|
||||
for (size_t k = 0; k < s.components.size(); ++k) {
|
||||
unsigned int comp_0based = s.components[k] - 1;
|
||||
grp.components_0based.push_back(comp_0based);
|
||||
}
|
||||
grp.sub_heights = sub_heights;
|
||||
|
||||
// Write gradient metadata (run-aware). Both per_object_gradient and
|
||||
// per_volume_gradient are populated independently from their own run-state
|
||||
// machines; the GCode emitter chooses per-region:
|
||||
// - tagged region (gradient_volume_id valid) -> per_volume_gradient[{obj, vol}]
|
||||
// - untagged region (modifier / painted / etc.) -> per_object_gradient[obj]
|
||||
// Populating both keeps the per-object run state correct even when per-volume
|
||||
// takes over for the same (slot, obj), and lets untagged geometry (which is
|
||||
// explicitly NOT split per-volume in v1 per the design doc) keep its legacy
|
||||
// per-object gradient ratios.
|
||||
if (grp.is_gradient) {
|
||||
auto vol_runs_slot_it = per_vol_runs.find(ext);
|
||||
if (vol_runs_slot_it != per_vol_runs.end()) {
|
||||
auto vol_slot_it = m_gradient_volume_layers.find(ext);
|
||||
for (auto &[vkey, st] : vol_runs_slot_it->second) {
|
||||
auto &layer_indices = vol_slot_it->second[vkey];
|
||||
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
|
||||
continue;
|
||||
if (st.current_run < 0 ||
|
||||
st.current_idx >= st.run_lengths[st.current_run]) {
|
||||
st.current_run++;
|
||||
st.current_idx = 0;
|
||||
}
|
||||
size_t run_N = st.run_lengths[st.current_run];
|
||||
size_t run_idx = st.current_idx++;
|
||||
grp.per_volume_gradient[vkey] = {
|
||||
run_N,
|
||||
run_idx,
|
||||
gradient_info[ext].start,
|
||||
gradient_info[ext].end_val,
|
||||
gradient_info[ext].curve,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
auto runs_slot_it = per_obj_runs.find(ext);
|
||||
if (runs_slot_it != per_obj_runs.end()) {
|
||||
auto slot_it = m_mixed_object_layers.find(ext);
|
||||
for (auto &[obj, st] : runs_slot_it->second) {
|
||||
auto &layer_indices = slot_it->second[obj];
|
||||
if (!std::binary_search(layer_indices.begin(), layer_indices.end(), layer_idx))
|
||||
continue;
|
||||
if (st.current_run < 0 ||
|
||||
st.current_idx >= st.run_lengths[st.current_run]) {
|
||||
st.current_run++;
|
||||
st.current_idx = 0;
|
||||
}
|
||||
size_t run_N = st.run_lengths[st.current_run];
|
||||
size_t run_idx = st.current_idx++;
|
||||
grp.per_object_gradient[obj] = {
|
||||
run_N,
|
||||
run_idx,
|
||||
gradient_info[ext].start,
|
||||
gradient_info[ext].end_val,
|
||||
gradient_info[ext].curve,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (grp.components_0based.size() > 1) {
|
||||
unsigned int first_comp_0based = s.components[0] - 1;
|
||||
std::vector<size_t> idx(grp.components_0based.size());
|
||||
std::iota(idx.begin(), idx.end(), 0);
|
||||
std::sort(idx.begin(), idx.end(), [&](size_t a, size_t b) {
|
||||
return grp.components_0based[a] < grp.components_0based[b];
|
||||
});
|
||||
std::vector<unsigned int> sorted_comps;
|
||||
std::vector<double> sorted_heights;
|
||||
for (size_t i : idx) {
|
||||
sorted_comps.push_back(grp.components_0based[i]);
|
||||
sorted_heights.push_back(grp.sub_heights[i]);
|
||||
}
|
||||
grp.components_0based = std::move(sorted_comps);
|
||||
grp.sub_heights = std::move(sorted_heights);
|
||||
if (grp.is_gradient) {
|
||||
for (size_t i = 0; i < grp.components_0based.size(); ++i) {
|
||||
if (grp.components_0based[i] == first_comp_0based) {
|
||||
grp.gradient_first_sorted_idx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned int comp : grp.components_0based)
|
||||
new_extruders.push_back(comp);
|
||||
lt.mixed_sub_layer_groups.push_back(std::move(grp));
|
||||
prev_print_z_for_slot[ext] = lt.print_z;
|
||||
} else {
|
||||
// Deficit Round-Robin: pick one component per layer.
|
||||
// Weight by layer height so volume ratios stay accurate
|
||||
// even with adaptive layer heights.
|
||||
double lh = calc_slot_lh(ext, lt.print_z);
|
||||
long long lh_i = std::llround(lh * 1e6);
|
||||
|
||||
// For 2-component gradient on the first layer, use the gradient's
|
||||
// starting ratio instead of the configured mixing ratio so the
|
||||
// selected filament matches the gradient's "from" end.
|
||||
// Only affects the first layer; when sublayer splitting is enabled
|
||||
// (required for gradient), layers 1+ take the sublayer path and
|
||||
// do not touch the DRR accumulator.
|
||||
if (layer_idx == 0 && is_gradient[ext] && s.components.size() == 2) {
|
||||
double r0 = gradient_info[ext].start;
|
||||
s.accum[0] += std::llround(r0 * lh_i);
|
||||
s.accum[1] += std::llround((1.0 - r0) * lh_i);
|
||||
} else {
|
||||
for (size_t k = 0; k < s.ratios.size(); ++k)
|
||||
s.accum[k] += std::llround(s.ratios[k] * lh_i);
|
||||
}
|
||||
size_t sel = 0;
|
||||
for (size_t k = 1; k < s.accum.size(); ++k)
|
||||
if (s.accum[k] > s.accum[sel])
|
||||
sel = k;
|
||||
s.accum[sel] -= lh_i;
|
||||
unsigned int resolved = s.components[sel] - 1;
|
||||
lt.mixed_filament_resolution[ext] = resolved;
|
||||
new_extruders.push_back(resolved);
|
||||
prev_print_z_for_slot[ext] = lt.print_z;
|
||||
}
|
||||
}
|
||||
lt.extruders = new_extruders;
|
||||
sort_remove_duplicates(lt.extruders);
|
||||
|
||||
// Update prev_relevant_z: for each slot that has relevant-layer tracking,
|
||||
// advance if the current layer belongs to a slot-owning object.
|
||||
for (auto &[slot, rel_set] : slot_relevant_layers) {
|
||||
if (rel_set.count(layer_idx))
|
||||
prev_relevant_z_for_slot[slot] = lt.print_z;
|
||||
}
|
||||
|
||||
prev_print_z = lt.print_z;
|
||||
}
|
||||
}
|
||||
|
||||
void ToolOrdering::enforce_mixed_component_order()
|
||||
{
|
||||
for (LayerTools < : m_layer_tools) {
|
||||
if (lt.mixed_sub_layer_groups.empty())
|
||||
continue;
|
||||
|
||||
// Build a set of extruders present in lt.extruders for fast lookup.
|
||||
std::set<unsigned int> ext_set(lt.extruders.begin(), lt.extruders.end());
|
||||
|
||||
// 1. Build DAG from mixed group constraints.
|
||||
// For each group [c0, c1, c2, ...], add edges c0->c1, c1->c2, ...
|
||||
// Only between components that are both present in lt.extruders.
|
||||
// Use an edge set to avoid duplicate edges inflating in-degree.
|
||||
std::map<unsigned int, std::vector<unsigned int>> adj;
|
||||
std::map<unsigned int, int> in_degree;
|
||||
std::set<std::pair<unsigned int, unsigned int>> edge_set;
|
||||
|
||||
for (unsigned int ext : lt.extruders)
|
||||
in_degree[ext] = 0;
|
||||
|
||||
for (const auto &grp : lt.mixed_sub_layer_groups) {
|
||||
for (size_t i = 0; i + 1 < grp.components_0based.size(); ++i) {
|
||||
unsigned int a = grp.components_0based[i];
|
||||
unsigned int b = grp.components_0based[i + 1];
|
||||
if (!ext_set.count(a) || !ext_set.count(b))
|
||||
continue;
|
||||
if (edge_set.insert({a, b}).second) {
|
||||
adj[a].push_back(b);
|
||||
in_degree[b] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Record original position (from flush optimizer) as priority.
|
||||
std::map<unsigned int, size_t> orig_pos;
|
||||
for (size_t i = 0; i < lt.extruders.size(); ++i)
|
||||
orig_pos[lt.extruders[i]] = i;
|
||||
|
||||
// 3. Kahn's topological sort with priority queue (prefer original position).
|
||||
auto cmp = [&orig_pos](unsigned int lhs, unsigned int rhs) {
|
||||
return orig_pos[lhs] > orig_pos[rhs]; // min-heap by orig_pos
|
||||
};
|
||||
std::priority_queue<unsigned int, std::vector<unsigned int>, decltype(cmp)> pq(cmp);
|
||||
|
||||
for (unsigned int ext : lt.extruders) {
|
||||
if (in_degree[ext] == 0)
|
||||
pq.push(ext);
|
||||
}
|
||||
|
||||
std::vector<unsigned int> ordered;
|
||||
ordered.reserve(lt.extruders.size());
|
||||
while (!pq.empty()) {
|
||||
unsigned int ext = pq.top();
|
||||
pq.pop();
|
||||
ordered.push_back(ext);
|
||||
if (auto it = adj.find(ext); it != adj.end()) {
|
||||
for (unsigned int next : it->second) {
|
||||
if (--in_degree[next] == 0)
|
||||
pq.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: if topological sort didn't produce all elements, keep original order.
|
||||
if (ordered.size() != lt.extruders.size())
|
||||
ordered = lt.extruders;
|
||||
|
||||
// 4. Verify: every mixed group's component order is preserved as subsequence.
|
||||
for (const auto &grp : lt.mixed_sub_layer_groups) {
|
||||
size_t prev_pos = 0;
|
||||
bool valid = true;
|
||||
for (unsigned int c : grp.components_0based) {
|
||||
if (!ext_set.count(c))
|
||||
continue;
|
||||
auto it = std::find(ordered.begin() + prev_pos, ordered.end(), c);
|
||||
if (it == ordered.end()) { valid = false; break; }
|
||||
prev_pos = (it - ordered.begin()) + 1;
|
||||
}
|
||||
assert(valid && "enforce_mixed_component_order: mixed group subsequence violated");
|
||||
(void)valid;
|
||||
}
|
||||
|
||||
lt.extruders = ordered;
|
||||
}
|
||||
}
|
||||
|
||||
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
|
||||
{
|
||||
const PrintConfig* print_config = m_print_config_ptr;
|
||||
|
||||
Reference in New Issue
Block a user