mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +00:00
Add mixed filament bias controls and bump version to 0.9.51
This commit is contained in:
@@ -11,6 +11,7 @@ on:
|
||||
- 'version.inc'
|
||||
- 'CHANGELOG*.md'
|
||||
- 'RELEASE_NOTES*.md'
|
||||
- 'doc/changelogs/**'
|
||||
- 'localization/**'
|
||||
- 'resources/**'
|
||||
- ".github/workflows/build_*.yml"
|
||||
@@ -76,6 +77,8 @@ jobs:
|
||||
|
||||
notes_content=""
|
||||
for notes_file in \
|
||||
"doc/changelogs/CHANGELOG_v${version}.md" \
|
||||
"doc/changelogs/RELEASE_NOTES_v${version}.md" \
|
||||
"CHANGELOG_v${version}.md" \
|
||||
"RELEASE_NOTES_v${version}.md"; do
|
||||
if [ -f "$notes_file" ]; then
|
||||
|
||||
@@ -48,6 +48,7 @@ Snapmaker Orca FullSpectrum includes support for **virtual mixed-color filaments
|
||||
- Visual preview showing the additive color blend
|
||||
- Enable/disable individual mixed filaments
|
||||
- Per-layer resolution control with customizable ratios
|
||||
- Optional per-pair Bias control for slightly recessing one component to push the apparent color toward the other
|
||||
- Seamless integration with the existing filament management system
|
||||
|
||||
### Using Mixed Filaments
|
||||
@@ -61,6 +62,16 @@ Snapmaker Orca FullSpectrum includes support for **virtual mixed-color filaments
|
||||
4. Mixed filaments can be assigned to objects just like physical filaments
|
||||
5. During slicing, the mixed filament resolves to alternating layers of its components
|
||||
|
||||
### Bias Control
|
||||
Snapmaker Orca FullSpectrum also includes an optional **Bias** control for mixed filament pairs. When enabled in **Print Settings -> Others -> Mixed Filaments**, each mixed row gets a compact inline Bias value:
|
||||
|
||||
- **Positive Bias** recesses the second filament in the pair
|
||||
- **Negative Bias** recesses the first filament in the pair
|
||||
- This lets you shift the apparent color without changing the nominal layer cadence
|
||||
- The inline preview updates to show the estimated apparent mix shift
|
||||
|
||||
Example: for a pair like `F1 + F2`, a positive bias makes `F2` sit slightly lower, so `F1` visually dominates more. A negative bias does the opposite and recesses `F1`.
|
||||
|
||||
### Dithering Settings
|
||||
Snapmaker Orca FullSpectrum includes advanced dithering controls to fine-tune the layer alternation behavior for mixed filaments. These settings are found in **Others → Dithering** in the print settings:
|
||||
|
||||
|
||||
@@ -1308,10 +1308,9 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
bool has_delimiters = true;
|
||||
{
|
||||
//BBS
|
||||
std::string bambuslicer_gcode_header = "; Snapmaker_Orca";
|
||||
|
||||
std::string Snapmaker_Orca_gcode_header = std::string("; generated by ");
|
||||
Snapmaker_Orca_gcode_header += SLIC3R_APP_NAME;
|
||||
std::string bambuslicer_gcode_header = "; Snapmaker_Orca";
|
||||
std::string legacy_fs_gcode_header = std::string("; generated by ") + SLIC3R_APP_NAME;
|
||||
std::string compat_snapmaker_gcode_header = "; generated by Snapmaker Orca";
|
||||
|
||||
std::string header;
|
||||
bool header_found = false;
|
||||
@@ -1323,7 +1322,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
|
||||
line_c = skip_whitespaces(line_c);
|
||||
// BBS
|
||||
if (strncmp(bambuslicer_gcode_header.c_str(), line_c, strlen(bambuslicer_gcode_header.c_str())) == 0 ||
|
||||
strncmp(Snapmaker_Orca_gcode_header.c_str(), line_c, strlen(Snapmaker_Orca_gcode_header.c_str())) == 0) {
|
||||
strncmp(legacy_fs_gcode_header.c_str(), line_c, strlen(legacy_fs_gcode_header.c_str())) == 0 ||
|
||||
strncmp(compat_snapmaker_gcode_header.c_str(), line_c, strlen(compat_snapmaker_gcode_header.c_str())) == 0) {
|
||||
header_found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -619,6 +619,7 @@ const std::vector<std::pair<GCodeProcessor::EProducer, std::string>> GCodeProces
|
||||
//BBS: Snapmaker_Orca is also "bambu". Otherwise the time estimation didn't work.
|
||||
//FIXME: Workaround and should be handled when do removing-bambu
|
||||
{ EProducer::Snapmaker_Orca, SLIC3R_APP_NAME },
|
||||
{ EProducer::Snapmaker_Orca, "generated by Snapmaker Orca" },
|
||||
{ EProducer::Snapmaker_Orca, "generated by Snapmaker_Orca" },
|
||||
{ EProducer::Snapmaker_Orca, "generated by BambuStudio" },
|
||||
{ EProducer::Snapmaker_Orca, "BambuStudio" }
|
||||
|
||||
@@ -216,6 +216,18 @@ static float clamp_surface_offset(float v)
|
||||
return std::clamp(v, -2.f, 2.f);
|
||||
}
|
||||
|
||||
static float canonical_signed_bias_value(float component_a_surface_offset, float component_b_surface_offset)
|
||||
{
|
||||
const float offset_a = clamp_surface_offset(component_a_surface_offset);
|
||||
const float offset_b = clamp_surface_offset(component_b_surface_offset);
|
||||
|
||||
if (std::abs(offset_b) > EPSILON)
|
||||
return offset_b;
|
||||
if (std::abs(offset_a) > EPSILON)
|
||||
return (offset_a >= 0.f) ? -std::abs(offset_a) : std::abs(offset_a);
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
static std::string format_surface_offset_token(float value)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
@@ -1444,10 +1456,11 @@ float MixedFilamentManager::component_surface_offset(unsigned int filament_id,
|
||||
layer_print_z,
|
||||
layer_height,
|
||||
force_height_weighted);
|
||||
if (resolved == mixed_row->component_a)
|
||||
return clamp_surface_offset(mixed_row->component_a_surface_offset);
|
||||
if (resolved == mixed_row->component_b)
|
||||
return clamp_surface_offset(mixed_row->component_b_surface_offset);
|
||||
const float signed_bias = canonical_signed_bias_value(mixed_row->component_a_surface_offset, mixed_row->component_b_surface_offset);
|
||||
if (signed_bias > EPSILON && resolved == mixed_row->component_b)
|
||||
return signed_bias;
|
||||
if (signed_bias < -EPSILON && resolved == mixed_row->component_a)
|
||||
return -signed_bias;
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
@@ -1591,14 +1604,48 @@ std::string MixedFilamentManager::blend_color(const std::string &color_a,
|
||||
return rgb_to_hex({int(out_r), int(out_g), int(out_b)});
|
||||
}
|
||||
|
||||
float MixedFilamentManager::max_component_surface_offset_mm(float reference_width_mm)
|
||||
{
|
||||
const float safe_reference = std::max(0.05f, std::abs(reference_width_mm));
|
||||
return std::clamp(safe_reference, 0.01f, 0.35f);
|
||||
}
|
||||
|
||||
float MixedFilamentManager::max_pair_bias_mm(float reference_width_mm)
|
||||
{
|
||||
return max_component_surface_offset_mm(reference_width_mm);
|
||||
}
|
||||
|
||||
std::pair<float, float> MixedFilamentManager::surface_offset_pair_from_signed_bias(float bias_mm,
|
||||
float reference_width_mm)
|
||||
{
|
||||
const float clamped_bias = std::clamp(bias_mm,
|
||||
-max_pair_bias_mm(reference_width_mm),
|
||||
max_pair_bias_mm(reference_width_mm));
|
||||
if (clamped_bias > EPSILON)
|
||||
return std::make_pair(0.f, clamped_bias);
|
||||
if (clamped_bias < -EPSILON)
|
||||
return std::make_pair(-clamped_bias, 0.f);
|
||||
return std::make_pair(0.f, 0.f);
|
||||
}
|
||||
|
||||
float MixedFilamentManager::bias_ui_value_from_surface_offsets(float component_a_surface_offset,
|
||||
float component_b_surface_offset,
|
||||
float reference_width_mm)
|
||||
{
|
||||
return std::clamp(canonical_signed_bias_value(component_a_surface_offset, component_b_surface_offset),
|
||||
-max_pair_bias_mm(reference_width_mm),
|
||||
max_pair_bias_mm(reference_width_mm));
|
||||
}
|
||||
|
||||
int MixedFilamentManager::apparent_mix_b_percent(int mix_b_percent,
|
||||
float component_a_surface_offset,
|
||||
float component_b_surface_offset,
|
||||
float reference_width_mm)
|
||||
{
|
||||
const float safe_reference = std::max(0.05f, std::abs(reference_width_mm));
|
||||
const float shift_pct = 100.f * (clamp_surface_offset(component_a_surface_offset) -
|
||||
clamp_surface_offset(component_b_surface_offset)) / safe_reference;
|
||||
const float shift_pct = -100.f * std::clamp(canonical_signed_bias_value(component_a_surface_offset, component_b_surface_offset),
|
||||
-max_pair_bias_mm(reference_width_mm),
|
||||
max_pair_bias_mm(reference_width_mm)) / safe_reference;
|
||||
return clamp_int(int(std::lround(float(clamp_int(mix_b_percent, 0, 100)) + shift_pct)), 0, 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -229,6 +229,13 @@ public:
|
||||
static std::string blend_color(const std::string &color_a,
|
||||
const std::string &color_b,
|
||||
int ratio_a, int ratio_b);
|
||||
static float max_component_surface_offset_mm(float reference_width_mm = 0.4f);
|
||||
static float max_pair_bias_mm(float reference_width_mm = 0.4f);
|
||||
static std::pair<float, float> surface_offset_pair_from_signed_bias(float bias_mm,
|
||||
float reference_width_mm = 0.4f);
|
||||
static float bias_ui_value_from_surface_offsets(float component_a_surface_offset,
|
||||
float component_b_surface_offset,
|
||||
float reference_width_mm = 0.4f);
|
||||
static int apparent_mix_b_percent(int mix_b_percent,
|
||||
float component_a_surface_offset,
|
||||
float component_b_surface_offset,
|
||||
|
||||
@@ -74,6 +74,7 @@ static std::vector<std::string> s_project_options {
|
||||
"mixed_filament_height_lower_bound",
|
||||
"mixed_filament_height_upper_bound",
|
||||
"mixed_filament_advanced_dithering",
|
||||
"mixed_filament_component_bias_enabled",
|
||||
"mixed_filament_surface_indentation",
|
||||
"mixed_filament_region_collapse",
|
||||
"mixed_filament_definitions",
|
||||
|
||||
@@ -296,6 +296,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "mixed_filament_height_lower_bound"
|
||||
|| opt_key == "mixed_filament_height_upper_bound"
|
||||
|| opt_key == "mixed_filament_advanced_dithering"
|
||||
|| opt_key == "mixed_filament_component_bias_enabled"
|
||||
|| opt_key == "mixed_filament_surface_indentation"
|
||||
|| opt_key == "mixed_filament_region_collapse"
|
||||
|| opt_key == "mixed_filament_definitions"
|
||||
|
||||
@@ -1208,6 +1208,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
new_full_config.option("mixed_filament_advanced_dithering", true);
|
||||
new_full_config.option("mixed_filament_pointillism_pixel_size", true);
|
||||
new_full_config.option("mixed_filament_pointillism_line_gap", true);
|
||||
new_full_config.option("mixed_filament_component_bias_enabled", true);
|
||||
new_full_config.option("mixed_filament_surface_indentation", true);
|
||||
new_full_config.option("mixed_filament_region_collapse", true);
|
||||
new_full_config.option("mixed_filament_definitions", true);
|
||||
@@ -1220,6 +1221,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
m_config.option("mixed_filament_advanced_dithering", true);
|
||||
m_config.option("mixed_filament_pointillism_pixel_size", true);
|
||||
m_config.option("mixed_filament_pointillism_line_gap", true);
|
||||
m_config.option("mixed_filament_component_bias_enabled", true);
|
||||
m_config.option("mixed_filament_surface_indentation", true);
|
||||
m_config.option("mixed_filament_region_collapse", true);
|
||||
m_config.option("mixed_filament_definitions", true);
|
||||
@@ -1232,6 +1234,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
m_default_object_config.option("mixed_filament_advanced_dithering", true);
|
||||
m_default_object_config.option("mixed_filament_pointillism_pixel_size", true);
|
||||
m_default_object_config.option("mixed_filament_pointillism_line_gap", true);
|
||||
m_default_object_config.option("mixed_filament_component_bias_enabled", true);
|
||||
m_default_object_config.option("mixed_filament_surface_indentation", true);
|
||||
m_default_object_config.option("mixed_filament_region_collapse", true);
|
||||
m_default_object_config.option("mixed_filament_definitions", true);
|
||||
|
||||
@@ -4232,6 +4232,15 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(0.0));
|
||||
|
||||
def = this->add("mixed_filament_component_bias_enabled", coBool);
|
||||
def->label = L("Enable mixed filament bias");
|
||||
def->category = L("Others");
|
||||
def->tooltip = L("Show and apply the per-row mixed filament Bias control.\n\n"
|
||||
"When enabled, the selected filament in a mixed pair is recessed slightly so the other component becomes more visible.\n\n"
|
||||
"Bias is ignored for grouped wall patterns, same-layer pointillisme, and Local Z dithering.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("mixed_filament_surface_indentation", coFloat);
|
||||
def->label = L("Selective Expansion contraction");
|
||||
def->category = L("Others");
|
||||
|
||||
@@ -1362,6 +1362,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionBool, mixed_filament_advanced_dithering))
|
||||
((ConfigOptionFloat, mixed_filament_pointillism_pixel_size))
|
||||
((ConfigOptionFloat, mixed_filament_pointillism_line_gap))
|
||||
((ConfigOptionBool, mixed_filament_component_bias_enabled))
|
||||
((ConfigOptionFloat, mixed_filament_surface_indentation))
|
||||
((ConfigOptionBool, mixed_filament_region_collapse))
|
||||
((ConfigOptionString, mixed_filament_definitions))
|
||||
|
||||
@@ -950,6 +950,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "dithering_z_step_size"
|
||||
|| opt_key == "dithering_local_z_mode"
|
||||
|| opt_key == "dithering_step_painted_zones_only"
|
||||
|| opt_key == "mixed_filament_component_bias_enabled"
|
||||
|| opt_key == "mixed_filament_region_collapse"
|
||||
|| opt_key == "mmu_segmented_region_max_width"
|
||||
|| opt_key == "mmu_segmented_region_interlocking_depth"
|
||||
@@ -973,6 +974,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "mixed_filament_height_lower_bound"
|
||||
|| opt_key == "mixed_filament_height_upper_bound"
|
||||
|| opt_key == "mixed_filament_advanced_dithering"
|
||||
|| opt_key == "mixed_filament_component_bias_enabled"
|
||||
|| opt_key == "mixed_filament_surface_indentation"
|
||||
|| opt_key == "mixed_filament_region_collapse"
|
||||
|| opt_key == "mixed_filament_definitions") {
|
||||
|
||||
@@ -897,6 +897,52 @@ static inline unsigned int segmentation_channel_filament_id(size_t channel_idx)
|
||||
return unsigned(channel_idx);
|
||||
}
|
||||
|
||||
static float mixed_filament_reference_nozzle_mm(const MixedFilament &mixed_row, const ConfigOptionFloats &nozzle_diameters)
|
||||
{
|
||||
std::vector<float> samples;
|
||||
samples.reserve(2);
|
||||
|
||||
auto append_if_valid = [&samples, &nozzle_diameters](unsigned int component_id) {
|
||||
if (component_id >= 1 && component_id <= nozzle_diameters.size())
|
||||
samples.emplace_back(std::max(0.05f, float(nozzle_diameters.get_at(component_id - 1))));
|
||||
};
|
||||
|
||||
append_if_valid(mixed_row.component_a);
|
||||
append_if_valid(mixed_row.component_b);
|
||||
|
||||
if (samples.empty())
|
||||
return 0.4f;
|
||||
return std::accumulate(samples.begin(), samples.end(), 0.0f) / float(samples.size());
|
||||
}
|
||||
|
||||
static coordf_t clamped_mixed_component_surface_offset(const MixedFilamentManager &mixed_mgr,
|
||||
const PrintConfig &print_cfg,
|
||||
unsigned int filament_id,
|
||||
size_t num_physical,
|
||||
int layer_index,
|
||||
float layer_print_z,
|
||||
float layer_height,
|
||||
bool force_height_weighted = false)
|
||||
{
|
||||
const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(filament_id, num_physical);
|
||||
if (mixed_row == nullptr)
|
||||
return 0.f;
|
||||
|
||||
const coordf_t offset_mm = coordf_t(mixed_mgr.component_surface_offset(
|
||||
filament_id,
|
||||
num_physical,
|
||||
layer_index,
|
||||
layer_print_z,
|
||||
layer_height,
|
||||
force_height_weighted));
|
||||
if (std::abs(offset_mm) <= EPSILON)
|
||||
return 0.f;
|
||||
|
||||
const float reference_nozzle = mixed_filament_reference_nozzle_mm(*mixed_row, print_cfg.nozzle_diameter);
|
||||
const coordf_t limit_mm = coordf_t(MixedFilamentManager::max_component_surface_offset_mm(reference_nozzle));
|
||||
return std::clamp(offset_mm, -limit_mm, limit_mm);
|
||||
}
|
||||
|
||||
static bool apply_mixed_surface_indentation(PrintObject &print_object, std::vector<std::vector<ExPolygons>> &segmentation)
|
||||
{
|
||||
const Print *print = print_object.print();
|
||||
@@ -1041,6 +1087,8 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std
|
||||
const DynamicPrintConfig &full_cfg = print->full_print_config();
|
||||
if (bool_from_full_config(full_cfg, "dithering_local_z_mode", print_cfg.dithering_local_z_mode.value))
|
||||
return false;
|
||||
if (!bool_from_full_config(full_cfg, "mixed_filament_component_bias_enabled", print_cfg.mixed_filament_component_bias_enabled.value))
|
||||
return false;
|
||||
|
||||
const size_t num_physical = print_cfg.filament_colour.size();
|
||||
const size_t num_channels = segmentation.front().size();
|
||||
@@ -1065,7 +1113,6 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std
|
||||
size_t emptied_states = 0;
|
||||
size_t expanded_states = 0;
|
||||
size_t contracted_states = 0;
|
||||
size_t overlap_clipped_states = 0;
|
||||
|
||||
for (size_t layer_id = 0; layer_id < segmentation.size(); ++layer_id) {
|
||||
if (segmentation[layer_id].size() != num_channels)
|
||||
@@ -1085,11 +1132,13 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std
|
||||
if (!mixed_mgr.is_mixed(state_id, num_physical))
|
||||
continue;
|
||||
|
||||
const coordf_t offset_mm = coordf_t(mixed_mgr.component_surface_offset(state_id,
|
||||
num_physical,
|
||||
int(layer_id),
|
||||
layer_print_z,
|
||||
layer_height));
|
||||
const coordf_t offset_mm = clamped_mixed_component_surface_offset(mixed_mgr,
|
||||
print_cfg,
|
||||
state_id,
|
||||
num_physical,
|
||||
int(layer_id),
|
||||
layer_print_z,
|
||||
layer_height);
|
||||
if (std::abs(offset_mm) <= EPSILON)
|
||||
continue;
|
||||
|
||||
@@ -1113,8 +1162,6 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std
|
||||
occupied_other = union_ex(occupied_other);
|
||||
if (!occupied_other.empty()) {
|
||||
ExPolygons clipped = diff_ex(adjusted, occupied_other, ApplySafetyOffset::Yes);
|
||||
if (std::abs(area(clipped)) + EPSILON < std::abs(area(adjusted)))
|
||||
++overlap_clipped_states;
|
||||
adjusted = std::move(clipped);
|
||||
if (!adjusted.empty() && adjusted.size() > 1)
|
||||
adjusted = union_ex(adjusted);
|
||||
@@ -1145,8 +1192,7 @@ static bool apply_mixed_component_surface_offsets(PrintObject &print_object, std
|
||||
<< " changed_states=" << changed_states
|
||||
<< " contracted_states=" << contracted_states
|
||||
<< " expanded_states=" << expanded_states
|
||||
<< " emptied_states=" << emptied_states
|
||||
<< " overlap_clipped_states=" << overlap_clipped_states;
|
||||
<< " emptied_states=" << emptied_states;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2311,6 +2357,8 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object)
|
||||
const DynamicPrintConfig &full_cfg = print->full_print_config();
|
||||
if (bool_from_full_config(full_cfg, "dithering_local_z_mode", print_cfg.dithering_local_z_mode.value))
|
||||
return false;
|
||||
if (!bool_from_full_config(full_cfg, "mixed_filament_component_bias_enabled", print_cfg.mixed_filament_component_bias_enabled.value))
|
||||
return false;
|
||||
|
||||
const size_t num_physical = print_cfg.filament_diameter.size();
|
||||
if (num_physical == 0)
|
||||
@@ -2355,11 +2403,13 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object)
|
||||
if (!mixed_mgr.is_mixed(filament_id, num_physical))
|
||||
continue;
|
||||
|
||||
const coordf_t offset_mm = coordf_t(mixed_mgr.component_surface_offset(filament_id,
|
||||
num_physical,
|
||||
int(layer_id),
|
||||
float(layer.print_z),
|
||||
float(layer.height)));
|
||||
const coordf_t offset_mm = clamped_mixed_component_surface_offset(mixed_mgr,
|
||||
print_cfg,
|
||||
filament_id,
|
||||
num_physical,
|
||||
int(layer_id),
|
||||
float(layer.print_z),
|
||||
float(layer.height));
|
||||
if (std::abs(offset_mm) <= EPSILON)
|
||||
continue;
|
||||
|
||||
@@ -2367,7 +2417,8 @@ static bool apply_mixed_region_surface_offsets(PrintObject &print_object)
|
||||
if (delta_scaled <= float(EPSILON))
|
||||
continue;
|
||||
|
||||
ExPolygons adjusted = offset_ex(to_expolygons(layerm->slices.surfaces), offset_mm > 0 ? -delta_scaled : delta_scaled);
|
||||
const ExPolygons original = to_expolygons(layerm->slices.surfaces);
|
||||
ExPolygons adjusted = offset_ex(original, offset_mm > 0 ? -delta_scaled : delta_scaled);
|
||||
if (!adjusted.empty() && adjusted.size() > 1)
|
||||
adjusted = union_ex(adjusted);
|
||||
|
||||
@@ -3255,11 +3306,13 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
|
||||
const coordf_t base_height = std::max<coordf_t>(0.01f, coordf_t(print_object.config().layer_height.value));
|
||||
const bool collapse_mixed_regions =
|
||||
bool_from_full_config(full_cfg, "mixed_filament_region_collapse", print_cfg.mixed_filament_region_collapse.value);
|
||||
const bool bias_mode_enabled =
|
||||
bool_from_full_config(full_cfg, "mixed_filament_component_bias_enabled", print_cfg.mixed_filament_component_bias_enabled.value);
|
||||
const MixedFilamentManager &mixed_mgr = print_object.print()->mixed_filament_manager();
|
||||
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, segmentation.size(), std::max(segmentation.size() / 128, size_t(1))),
|
||||
[&print_object, &segmentation, &mixed_mgr, num_physical, preferred_a, preferred_b, base_height, collapse_mixed_regions, throw_on_cancel](const tbb::blocked_range<size_t> &range) {
|
||||
[&print_object, &segmentation, &mixed_mgr, num_physical, preferred_a, preferred_b, base_height, collapse_mixed_regions, bias_mode_enabled, throw_on_cancel](const tbb::blocked_range<size_t> &range) {
|
||||
const auto &layer_ranges = print_object.shared_regions()->layer_ranges;
|
||||
double z = print_object.get_layer(int(range.begin()))->slice_z;
|
||||
auto it_layer_range = layer_range_first(layer_ranges, z);
|
||||
@@ -3334,6 +3387,7 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
|
||||
std::vector<int> missing_target_extruders;
|
||||
ExPolygons default_segmentation = num_channels > 0 ? std::move(segmentation[layer_id][0]) : ExPolygons();
|
||||
BoundingBox default_bbox;
|
||||
bool layer_has_component_bias = false;
|
||||
if (!default_segmentation.empty()) {
|
||||
default_bbox = get_extents(default_segmentation);
|
||||
layer_split = true;
|
||||
@@ -3358,11 +3412,37 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
|
||||
region.bbox = get_extents(region.expolygons);
|
||||
layer_split = true;
|
||||
}
|
||||
|
||||
if (!region.expolygons.empty() &&
|
||||
bias_mode_enabled &&
|
||||
mixed_mgr.is_mixed(channel_id, num_physical) &&
|
||||
std::abs(mixed_mgr.component_surface_offset(channel_id,
|
||||
num_physical,
|
||||
int(layer_id),
|
||||
float(layer.print_z),
|
||||
float(layer.height))) > EPSILON)
|
||||
layer_has_component_bias = true;
|
||||
}
|
||||
|
||||
if (!layer_split)
|
||||
continue;
|
||||
|
||||
ExPolygons layer_geometry_mask;
|
||||
BoundingBox layer_geometry_bbox;
|
||||
if (layer_has_component_bias) {
|
||||
if (!default_segmentation.empty())
|
||||
append(layer_geometry_mask, default_segmentation);
|
||||
for (const ByExtruder &segmented : by_extruder) {
|
||||
if (!segmented.expolygons.empty())
|
||||
append(layer_geometry_mask, segmented.expolygons);
|
||||
}
|
||||
if (!layer_geometry_mask.empty()) {
|
||||
if (layer_geometry_mask.size() > 1)
|
||||
layer_geometry_mask = closing_ex(union_ex(std::move(layer_geometry_mask)), scaled<float>(5. * EPSILON));
|
||||
layer_geometry_bbox = get_extents(layer_geometry_mask);
|
||||
}
|
||||
}
|
||||
|
||||
// Split LayerRegions by by_extruder regions.
|
||||
// layer_range.painted_regions are sorted by extruder ID and parent PrintObject region ID.
|
||||
auto it_painted_region_begin = layer_range.painted_regions.cbegin();
|
||||
@@ -3399,6 +3479,14 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
|
||||
it_painted_region_begin = it_first_painted_region;
|
||||
|
||||
const BoundingBox parent_layer_region_bbox = get_extents(parent_layer_region.slices.surfaces);
|
||||
const bool clamp_parent_to_geometry =
|
||||
layer_has_component_bias &&
|
||||
layer_geometry_bbox.defined &&
|
||||
parent_layer_region_bbox.overlap(layer_geometry_bbox);
|
||||
ExPolygons clamped_parent_expolygons;
|
||||
if (clamp_parent_to_geometry)
|
||||
clamped_parent_expolygons = intersection_ex(parent_layer_region.slices.surfaces, layer_geometry_mask);
|
||||
|
||||
int self_extruder_id = -1; // 1-based extruder ID
|
||||
ExPolygons explicit_self_expolygons;
|
||||
ExPolygons default_self_expolygons;
|
||||
@@ -3465,7 +3553,11 @@ static inline void apply_mm_segmentation(PrintObject &print_object, std::vector<
|
||||
}
|
||||
|
||||
// Trim slices of this LayerRegion with all the MM regions.
|
||||
Polygons mine = to_polygons(parent_layer_region.slices.surfaces);
|
||||
// Mixed bias can intentionally shrink a painted layer's true silhouette.
|
||||
// Clamp the parent region to the post-bias segmentation union so the
|
||||
// vacated area stays empty instead of falling back to the parent tool.
|
||||
Polygons mine = clamp_parent_to_geometry ? to_polygons(clamped_parent_expolygons) :
|
||||
to_polygons(parent_layer_region.slices.surfaces);
|
||||
for (size_t extruder_idx = 0; extruder_idx < by_extruder.size(); ++extruder_idx) {
|
||||
const ByExtruder &segmented = by_extruder[extruder_idx];
|
||||
if (!assigned_extruder[extruder_idx])
|
||||
|
||||
@@ -1173,7 +1173,8 @@ std::string string_printf(const char *format, ...)
|
||||
|
||||
std::string header_slic3r_generated()
|
||||
{
|
||||
return std::string(SLIC3R_APP_NAME " " Snapmaker_VERSION);
|
||||
// Keep generated G-code branded like Snapmaker Orca for printer-side compatibility.
|
||||
return std::string("Snapmaker Orca ") + Snapmaker_VERSION;
|
||||
}
|
||||
|
||||
std::string header_gcodeviewer_generated()
|
||||
|
||||
@@ -9364,10 +9364,12 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
|
||||
for (const LayerRegion* layerm : layer->regions()) {
|
||||
if (layerm->slices.surfaces.empty())
|
||||
continue;
|
||||
const PrintRegionConfig& cfg = layerm->region().config();
|
||||
if (cfg.wall_filament.value == m_selected_extruder ||
|
||||
cfg.sparse_infill_filament.value == m_selected_extruder ||
|
||||
cfg.solid_infill_filament.value == m_selected_extruder ) {
|
||||
const int effective_wall_filament = int(layerm->extruder(frPerimeter));
|
||||
const int effective_sparse_infill_filament = int(layerm->extruder(frInfill));
|
||||
const int effective_solid_infill_filament = int(layerm->extruder(frSolidInfill));
|
||||
if (effective_wall_filament == m_selected_extruder ||
|
||||
effective_sparse_infill_filament == m_selected_extruder ||
|
||||
effective_solid_infill_filament == m_selected_extruder) {
|
||||
at_least_one_has_correct_extruder = true;
|
||||
break;
|
||||
}
|
||||
@@ -9389,16 +9391,19 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
|
||||
for (const LayerRegion *layerm : layer->regions()) {
|
||||
if (is_selected_separate_extruder)
|
||||
{
|
||||
const PrintRegionConfig& cfg = layerm->region().config();
|
||||
const int effective_wall_filament = int(layerm->extruder(frPerimeter));
|
||||
const int effective_sparse_infill_filament = int(layerm->extruder(frInfill));
|
||||
if (cfg.wall_filament.value != m_selected_extruder &&
|
||||
const int effective_solid_infill_filament = int(layerm->extruder(frSolidInfill));
|
||||
if (effective_wall_filament != m_selected_extruder &&
|
||||
effective_sparse_infill_filament != m_selected_extruder &&
|
||||
cfg.solid_infill_filament.value != m_selected_extruder)
|
||||
effective_solid_infill_filament != m_selected_extruder)
|
||||
continue;
|
||||
}
|
||||
if (ctxt.has_perimeters)
|
||||
if (ctxt.has_perimeters) {
|
||||
const int effective_wall_filament = int(layerm->extruder(frPerimeter));
|
||||
_3DScene::extrusionentity_to_verts(layerm->perimeters, float(layer->print_z), copy,
|
||||
select_geometry(idx_layer, layerm->region().config().wall_filament.value, 0));
|
||||
select_geometry(idx_layer, effective_wall_filament, 0));
|
||||
}
|
||||
if (ctxt.has_infill) {
|
||||
for (const ExtrusionEntity *ee : layerm->fills.entities) {
|
||||
// fill represents infill extrusions of a single island.
|
||||
@@ -9406,13 +9411,14 @@ void GLCanvas3D::_load_print_object_toolpaths(const PrintObject& print_object, c
|
||||
if (! fill->entities.empty())
|
||||
{
|
||||
const int effective_sparse_infill_filament = int(layerm->extruder(frInfill));
|
||||
const int effective_solid_infill_filament = int(layerm->extruder(frSolidInfill));
|
||||
_3DScene::extrusionentity_to_verts(*fill, float(layer->print_z), copy,
|
||||
select_geometry(idx_layer,
|
||||
(fill->entities.front()->role() == erSolidInfill &&
|
||||
std::abs(layerm->region().config().sparse_infill_density.value - 100.) < EPSILON) ?
|
||||
int(layerm->extruder(frSolidInfill)) :
|
||||
effective_solid_infill_filament :
|
||||
(is_solid_infill(fill->entities.front()->role()) ?
|
||||
layerm->region().config().solid_infill_filament :
|
||||
effective_solid_infill_filament :
|
||||
effective_sparse_infill_filament),
|
||||
1));
|
||||
}
|
||||
|
||||
+206
-108
@@ -4583,6 +4583,7 @@ public:
|
||||
const std::vector<double> &nozzle_diameters,
|
||||
const std::vector<wxColour> &palette,
|
||||
const MixedFilamentPreviewSettings &preview_settings,
|
||||
bool bias_mode_enabled,
|
||||
OnChangeFn on_change = {});
|
||||
|
||||
// Get the updated mixed filament data
|
||||
@@ -4604,6 +4605,7 @@ private:
|
||||
std::vector<double> m_nozzle_diameters;
|
||||
std::vector<wxColour> m_palette;
|
||||
MixedFilamentPreviewSettings m_preview_settings;
|
||||
bool m_bias_mode_enabled = false;
|
||||
bool m_has_changes = false;
|
||||
|
||||
wxChoice *m_choice_a = nullptr;
|
||||
@@ -4622,8 +4624,9 @@ private:
|
||||
wxStaticText *m_picker_b_label = nullptr;
|
||||
wxStaticText *m_picker_c_label = nullptr;
|
||||
wxStaticText *m_picker_d_label = nullptr;
|
||||
wxPanel *m_surface_offset_target_container = nullptr;
|
||||
wxPanel *m_surface_offset_target_swatch = nullptr;
|
||||
wxChoice *m_surface_offset_target_choice = nullptr;
|
||||
wxStaticText *m_surface_offset_target_label = nullptr;
|
||||
MixedGradientSelector *m_blend_selector = nullptr;
|
||||
wxStaticText *m_blend_label = nullptr;
|
||||
wxTextCtrl *m_pattern_ctrl = nullptr;
|
||||
@@ -4711,17 +4714,26 @@ private:
|
||||
return m_fallback;
|
||||
}
|
||||
|
||||
int vertical_inset_for_extruder(unsigned int extruder_id, int rect_height) const
|
||||
double max_active_surface_offset_mm() const
|
||||
{
|
||||
if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || rect_height <= 2)
|
||||
double max_offset = 0.0;
|
||||
for (double offset_mm : m_surface_offsets_mm)
|
||||
max_offset = std::max(max_offset, std::abs(offset_mm));
|
||||
return std::max(0.001, max_offset);
|
||||
}
|
||||
|
||||
int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const
|
||||
{
|
||||
if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2)
|
||||
return 0;
|
||||
|
||||
const double offset_mm = m_surface_offsets_mm[extruder_id];
|
||||
if (offset_mm <= 0.0)
|
||||
if (std::abs(offset_mm) <= EPSILON)
|
||||
return 0;
|
||||
|
||||
const double normalized = std::clamp(offset_mm / 2.0, 0.0, 1.0);
|
||||
return std::clamp(int(std::round(normalized * rect_height * 0.30)), 0, std::max(0, rect_height / 3));
|
||||
const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0);
|
||||
const int inset = int(std::round(normalized * slot_extent * 0.45)) * (offset_mm < 0.0 ? -1 : 1);
|
||||
return std::clamp(inset, -std::max(0, slot_extent / 2), std::max(0, slot_extent / 2));
|
||||
}
|
||||
|
||||
void on_paint(wxPaintEvent &)
|
||||
@@ -4747,8 +4759,11 @@ private:
|
||||
dc.SetBrush(wxBrush(color_for_extruder(m_sequence[idx])));
|
||||
const int x = rect.GetLeft() + s * stripe_w;
|
||||
const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w;
|
||||
const int inset_y = vertical_inset_for_extruder(extruder_id, rect.GetHeight());
|
||||
dc.DrawRectangle(x, rect.GetTop() + inset_y, std::max(1, w), std::max(1, rect.GetHeight() - inset_y * 2));
|
||||
const int inset = slot_inset_for_extruder(extruder_id, w);
|
||||
wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight());
|
||||
draw_rect.Intersect(rect);
|
||||
if (draw_rect.GetWidth() > 0)
|
||||
dc.DrawRectangle(draw_rect);
|
||||
}
|
||||
} else {
|
||||
const int bars = 24;
|
||||
@@ -4763,8 +4778,11 @@ private:
|
||||
dc.SetBrush(wxBrush(color_for_extruder(extruder_id)));
|
||||
const int x = rect.GetLeft() + i * bar_w;
|
||||
const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w;
|
||||
const int inset_y = vertical_inset_for_extruder(extruder_id, rect.GetHeight());
|
||||
dc.DrawRectangle(x, rect.GetTop() + inset_y, std::max(1, w), std::max(1, rect.GetHeight() - inset_y * 2));
|
||||
const int inset = slot_inset_for_extruder(extruder_id, w);
|
||||
wxRect draw_rect(x + inset / 2, rect.GetTop(), std::max(1, w - inset), rect.GetHeight());
|
||||
draw_rect.Intersect(rect);
|
||||
if (draw_rect.GetWidth() > 0)
|
||||
dc.DrawRectangle(draw_rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5582,8 +5600,11 @@ int MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(const Mixe
|
||||
}
|
||||
|
||||
static bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf,
|
||||
const MixedFilamentPreviewSettings &preview_settings)
|
||||
const MixedFilamentPreviewSettings &preview_settings,
|
||||
bool bias_mode_enabled)
|
||||
{
|
||||
if (!bias_mode_enabled)
|
||||
return false;
|
||||
if (preview_settings.local_z_mode)
|
||||
return false;
|
||||
if (mf.distribution_mode == int(MixedFilament::SameLayerPointillisme))
|
||||
@@ -5595,24 +5616,6 @@ static bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf,
|
||||
return mf.component_a >= 1 && mf.component_b >= 1 && mf.component_a != mf.component_b;
|
||||
}
|
||||
|
||||
static std::pair<int, float> mixed_filament_single_surface_offset_ui_state(const MixedFilament &mf)
|
||||
{
|
||||
const float offset_a = mf.component_a_surface_offset;
|
||||
const float offset_b = mf.component_b_surface_offset;
|
||||
const bool has_a = std::abs(offset_a) > 1e-6f;
|
||||
const bool has_b = std::abs(offset_b) > 1e-6f;
|
||||
|
||||
if (has_b && (!has_a || std::abs(offset_b) > std::abs(offset_a)))
|
||||
return { 1, offset_b };
|
||||
return { 0, has_a ? offset_a : 0.f };
|
||||
}
|
||||
|
||||
static std::pair<float, float> mixed_filament_single_surface_offset_pair(int target_index, float value)
|
||||
{
|
||||
const float clamped = std::clamp(value, -2.f, 2.f);
|
||||
return target_index == 1 ? std::make_pair(0.f, clamped) : std::make_pair(clamped, 0.f);
|
||||
}
|
||||
|
||||
static double mixed_filament_reference_nozzle_mm(unsigned int component_a,
|
||||
unsigned int component_b,
|
||||
const std::vector<double> &nozzle_diameters)
|
||||
@@ -5633,12 +5636,37 @@ static double mixed_filament_reference_nozzle_mm(unsigned int comp
|
||||
return std::accumulate(samples.begin(), samples.end(), 0.0) / double(samples.size());
|
||||
}
|
||||
|
||||
static double mixed_filament_bias_limit_mm(const MixedFilament &mf, const std::vector<double> &nozzle_diameters)
|
||||
{
|
||||
const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters);
|
||||
return MixedFilamentManager::max_pair_bias_mm(float(reference_nozzle_mm));
|
||||
}
|
||||
|
||||
static float mixed_filament_single_surface_offset_value(const MixedFilament &mf,
|
||||
const std::vector<double> &nozzle_diameters)
|
||||
{
|
||||
const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters);
|
||||
return MixedFilamentManager::bias_ui_value_from_surface_offsets(
|
||||
mf.component_a_surface_offset,
|
||||
mf.component_b_surface_offset,
|
||||
float(reference_nozzle_mm));
|
||||
}
|
||||
|
||||
static std::pair<float, float> mixed_filament_single_surface_offset_pair(const MixedFilament &mf,
|
||||
float value,
|
||||
const std::vector<double> &nozzle_diameters)
|
||||
{
|
||||
const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters);
|
||||
return MixedFilamentManager::surface_offset_pair_from_signed_bias(value, float(reference_nozzle_mm));
|
||||
}
|
||||
|
||||
static std::pair<int, int> mixed_filament_apparent_pair_percentages(const MixedFilament &mf,
|
||||
const MixedFilamentPreviewSettings &preview_settings,
|
||||
const std::vector<double> &nozzle_diameters)
|
||||
const std::vector<double> &nozzle_diameters,
|
||||
bool bias_mode_enabled)
|
||||
{
|
||||
const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings);
|
||||
if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings))
|
||||
if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled))
|
||||
return { 100 - base_b, base_b };
|
||||
|
||||
const double reference_nozzle_mm = mixed_filament_reference_nozzle_mm(mf.component_a, mf.component_b, nozzle_diameters);
|
||||
@@ -5651,15 +5679,16 @@ static std::pair<int, int> mixed_filament_apparent_pair_percentages(const MixedF
|
||||
|
||||
static std::string mixed_filament_apparent_pair_summary(const MixedFilament &mf,
|
||||
const MixedFilamentPreviewSettings &preview_settings,
|
||||
const std::vector<double> &nozzle_diameters)
|
||||
const std::vector<double> &nozzle_diameters,
|
||||
bool bias_mode_enabled)
|
||||
{
|
||||
if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings))
|
||||
if (!mixed_filament_supports_bias_apparent_color(mf, preview_settings, bias_mode_enabled))
|
||||
return {};
|
||||
|
||||
const int base_b = MixedFilamentConfigPanel::effective_local_z_preview_mix_b_percent(mf, preview_settings);
|
||||
const int base_a = 100 - base_b;
|
||||
const auto [apparent_a, apparent_b] =
|
||||
mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters);
|
||||
mixed_filament_apparent_pair_percentages(mf, preview_settings, nozzle_diameters, bias_mode_enabled);
|
||||
|
||||
if (std::abs(mf.component_a_surface_offset - mf.component_b_surface_offset) > 1e-4f &&
|
||||
(apparent_a != base_a || apparent_b != base_b)) {
|
||||
@@ -5883,6 +5912,7 @@ MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent,
|
||||
const std::vector<double> &nozzle_diameters,
|
||||
const std::vector<wxColour> &palette,
|
||||
const MixedFilamentPreviewSettings &preview_settings,
|
||||
bool bias_mode_enabled,
|
||||
OnChangeFn on_change)
|
||||
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL | wxBORDER_NONE)
|
||||
, m_mixed_id(mixed_id)
|
||||
@@ -5892,6 +5922,7 @@ MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent,
|
||||
, m_nozzle_diameters(nozzle_diameters)
|
||||
, m_palette(palette)
|
||||
, m_preview_settings(preview_settings)
|
||||
, m_bias_mode_enabled(bias_mode_enabled)
|
||||
, m_selected_weight_state(std::make_shared<std::vector<int>>())
|
||||
, m_on_change(on_change)
|
||||
{
|
||||
@@ -5905,6 +5936,7 @@ MixedFilamentConfigPanel::MixedFilamentConfigPanel(wxWindow *parent,
|
||||
void MixedFilamentConfigPanel::build_ui()
|
||||
{
|
||||
const int gap = FromDIP(6);
|
||||
const int compact_gap = std::max(FromDIP(2), gap / 3);
|
||||
const bool is_dark = wxGetApp().dark_mode();
|
||||
const wxColour panel_bg = GetBackgroundColour().IsOk() ? GetBackgroundColour() :
|
||||
(is_dark ? wxColour(52, 52, 56) : wxColour(255, 255, 255));
|
||||
@@ -5967,8 +5999,10 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
auto *container_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
swatch_out = new wxPanel(container_out, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(12), FromDIP(12)), wxBORDER_SIMPLE);
|
||||
swatch_out->SetMinSize(wxSize(FromDIP(12), FromDIP(12)));
|
||||
swatch_out->SetToolTip(tooltip);
|
||||
label_out = new wxStaticText(container_out, wxID_ANY, wxEmptyString);
|
||||
label_out->SetForegroundColour(local_picker_text);
|
||||
label_out->SetToolTip(tooltip);
|
||||
|
||||
auto *content_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
content_sizer->Add(swatch_out, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, inner_gap);
|
||||
@@ -6087,59 +6121,68 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
auto *preview_row = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_mix_preview = new MixedMixPreview(this);
|
||||
m_mix_preview->SetBackgroundColour(panel_bg);
|
||||
preview_row->Add(m_mix_preview, 3, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, gap);
|
||||
preview_row->Add(m_mix_preview, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap);
|
||||
|
||||
auto *bias_controls = new wxBoxSizer(wxHORIZONTAL);
|
||||
const auto initial_surface_offset_state = mixed_filament_single_surface_offset_ui_state(m_mf);
|
||||
const float initial_surface_offset_value = mixed_filament_single_surface_offset_value(m_mf, m_nozzle_diameters);
|
||||
const double initial_bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters);
|
||||
const wxString bias_tooltip =
|
||||
_L("Positive bias recesses the second filament in the pair; negative bias recesses the first filament.\n\n"
|
||||
"The color chip shows which filament the current value affects.\n\n"
|
||||
"Grouped wall patterns, same-layer pointillisme, and Local-Z dithering ignore it.");
|
||||
|
||||
auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("XY-Bias"));
|
||||
auto *surface_offset_label = new wxStaticText(this, wxID_ANY, _L("Bias"));
|
||||
surface_offset_label->SetForegroundColour(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20));
|
||||
surface_offset_label->SetToolTip(
|
||||
_L("Additional XY offset applied only on layers where this mixed filament resolves to component A or B.\n\n"
|
||||
"Positive values contract inward. Negative values expand outward.\n\n"
|
||||
"Currently this is intended for ordinary layer/height cadence rows. Grouped wall patterns, same-layer pointillisme, and Local-Z dithering ignore it."));
|
||||
bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, std::max(FromDIP(4), gap / 2));
|
||||
surface_offset_label->SetToolTip(bias_tooltip);
|
||||
bias_controls->Add(surface_offset_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap);
|
||||
|
||||
auto *target_group = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_surface_offset_target_swatch = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(10), FromDIP(10)), wxBORDER_SIMPLE);
|
||||
m_surface_offset_target_swatch->SetMinSize(wxSize(FromDIP(10), FromDIP(10)));
|
||||
m_surface_offset_target_swatch->SetMaxSize(wxSize(FromDIP(10), FromDIP(10)));
|
||||
m_surface_offset_target_swatch->SetToolTip(_L("Choose which filament in this pair receives the XY bias."));
|
||||
target_group->Add(m_surface_offset_target_swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(3));
|
||||
create_component_picker(m_surface_offset_target_container,
|
||||
m_surface_offset_target_swatch,
|
||||
m_surface_offset_target_label,
|
||||
bias_tooltip);
|
||||
if (m_surface_offset_target_container)
|
||||
m_surface_offset_target_container->SetCursor(wxCursor(wxCURSOR_ARROW));
|
||||
if (m_surface_offset_target_swatch)
|
||||
m_surface_offset_target_swatch->SetCursor(wxCursor(wxCURSOR_ARROW));
|
||||
if (m_surface_offset_target_label)
|
||||
m_surface_offset_target_label->SetCursor(wxCursor(wxCURSOR_ARROW));
|
||||
bias_controls->Add(m_surface_offset_target_container, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap);
|
||||
|
||||
wxArrayString surface_offset_targets;
|
||||
surface_offset_targets.Add(wxString::Format("F%d", int(m_mf.component_a)));
|
||||
surface_offset_targets.Add(wxString::Format("F%d", int(m_mf.component_b)));
|
||||
m_surface_offset_target_choice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(62), -1), surface_offset_targets);
|
||||
m_surface_offset_target_choice->SetSelection(std::clamp(initial_surface_offset_state.first, 0, 1));
|
||||
m_surface_offset_target_choice->SetToolTip(_L("Choose which filament in this pair receives the XY bias."));
|
||||
target_group->Add(m_surface_offset_target_choice, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
|
||||
bias_controls->Add(target_group, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
|
||||
|
||||
m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(64), -1),
|
||||
m_surface_offset_spin = new wxSpinCtrlDouble(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(58), -1),
|
||||
wxSP_ARROW_KEYS | wxALIGN_RIGHT | wxTE_PROCESS_ENTER,
|
||||
-2.0, 2.0, std::clamp(double(initial_surface_offset_state.second), -2.0, 2.0), 0.01);
|
||||
-initial_bias_limit, initial_bias_limit,
|
||||
std::clamp(double(initial_surface_offset_value), -initial_bias_limit, initial_bias_limit), 0.001);
|
||||
m_surface_offset_spin->SetDigits(3);
|
||||
m_surface_offset_spin->SetToolTip(
|
||||
_L("XY bias for the selected filament on layers where this row resolves to it. Positive values contract inward. Negative values expand outward."));
|
||||
bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
|
||||
m_surface_offset_spin->SetToolTip(bias_tooltip);
|
||||
bias_controls->Add(m_surface_offset_spin, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, compact_gap);
|
||||
|
||||
auto *surface_offset_units = new wxStaticText(this, wxID_ANY, _L("mm"));
|
||||
surface_offset_units->SetForegroundColour(is_dark ? wxColour(210, 210, 210) : wxColour(72, 72, 72));
|
||||
surface_offset_units->SetToolTip(bias_tooltip);
|
||||
bias_controls->Add(surface_offset_units, 0, wxALIGN_CENTER_VERTICAL);
|
||||
preview_row->Add(bias_controls, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL);
|
||||
if (m_bias_mode_enabled)
|
||||
preview_row->Add(bias_controls, 0, wxALIGN_CENTER_VERTICAL);
|
||||
else {
|
||||
surface_offset_label->Hide();
|
||||
if (m_surface_offset_target_container)
|
||||
m_surface_offset_target_container->Hide();
|
||||
if (m_surface_offset_spin)
|
||||
m_surface_offset_spin->Hide();
|
||||
surface_offset_units->Hide();
|
||||
}
|
||||
root->Add(preview_row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, gap);
|
||||
|
||||
const auto initial_surface_offset_pair =
|
||||
mixed_filament_single_surface_offset_pair(initial_surface_offset_state.first, initial_surface_offset_state.second);
|
||||
m_mf.component_a_surface_offset = initial_surface_offset_pair.first;
|
||||
m_mf.component_b_surface_offset = initial_surface_offset_pair.second;
|
||||
if (m_bias_mode_enabled) {
|
||||
const auto initial_surface_offset_pair =
|
||||
mixed_filament_single_surface_offset_pair(m_mf, initial_surface_offset_value, m_nozzle_diameters);
|
||||
m_mf.component_a_surface_offset = initial_surface_offset_pair.first;
|
||||
m_mf.component_b_surface_offset = initial_surface_offset_pair.second;
|
||||
}
|
||||
|
||||
const bool initial_component_surface_offsets_supported = !pattern_row_mode &&
|
||||
const bool initial_component_surface_offsets_supported = m_bias_mode_enabled &&
|
||||
!pattern_row_mode &&
|
||||
row_distribution_mode != int(MixedFilament::SameLayerPointillisme) &&
|
||||
!m_preview_settings.local_z_mode;
|
||||
if (m_surface_offset_target_choice)
|
||||
m_surface_offset_target_choice->Enable(initial_component_surface_offsets_supported);
|
||||
if (m_surface_offset_spin)
|
||||
m_surface_offset_spin->Enable(initial_component_surface_offsets_supported);
|
||||
|
||||
@@ -6179,6 +6222,18 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
auto apply_changes = [this]() {
|
||||
m_has_changes = true;
|
||||
|
||||
double surface_offset_value = 0.0;
|
||||
if (m_surface_offset_spin) {
|
||||
surface_offset_value = m_surface_offset_spin->GetValue();
|
||||
#if !defined(wxHAS_NATIVE_SPINCTRLDOUBLE)
|
||||
if (wxTextCtrl *text = m_surface_offset_spin->GetText()) {
|
||||
double parsed_value = 0.0;
|
||||
if (text->GetValue().ToDouble(&parsed_value))
|
||||
surface_offset_value = parsed_value;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int a = std::clamp(m_choice_a->GetSelection() + 1, 1, int(m_num_physical));
|
||||
int b = std::clamp(m_choice_b->GetSelection() + 1, 1, int(m_num_physical));
|
||||
if (a == b && m_num_physical > 1) {
|
||||
@@ -6194,14 +6249,16 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
const bool preserve_same_layer_mode = m_mf.distribution_mode == int(MixedFilament::SameLayerPointillisme);
|
||||
m_mf.component_a = unsigned(a);
|
||||
m_mf.component_b = unsigned(b);
|
||||
const int surface_offset_target_index =
|
||||
m_surface_offset_target_choice ? std::clamp(m_surface_offset_target_choice->GetSelection(), 0, 1) : 0;
|
||||
const float surface_offset_value =
|
||||
m_surface_offset_spin ? std::clamp(float(m_surface_offset_spin->GetValue()), -2.f, 2.f) : 0.f;
|
||||
const auto surface_offset_pair =
|
||||
mixed_filament_single_surface_offset_pair(surface_offset_target_index, surface_offset_value);
|
||||
m_mf.component_a_surface_offset = surface_offset_pair.first;
|
||||
m_mf.component_b_surface_offset = surface_offset_pair.second;
|
||||
if (m_bias_mode_enabled) {
|
||||
const double bias_limit = mixed_filament_bias_limit_mm(m_mf, m_nozzle_diameters);
|
||||
const float clamped_surface_offset_value = std::clamp(float(surface_offset_value), -float(bias_limit), float(bias_limit));
|
||||
const auto surface_offset_pair =
|
||||
mixed_filament_single_surface_offset_pair(m_mf, clamped_surface_offset_value, m_nozzle_diameters);
|
||||
m_mf.component_a_surface_offset = surface_offset_pair.first;
|
||||
m_mf.component_b_surface_offset = surface_offset_pair.second;
|
||||
if (m_surface_offset_spin)
|
||||
m_surface_offset_spin->SetValue(clamped_surface_offset_value);
|
||||
}
|
||||
m_mf.local_z_max_sublayers =
|
||||
(m_local_z_limit_checkbox != nullptr && m_local_z_limit_checkbox->GetValue() && m_local_z_limit_spin != nullptr) ?
|
||||
std::max(2, m_local_z_limit_spin->GetValue()) :
|
||||
@@ -6290,11 +6347,10 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
m_mf.custom = true;
|
||||
|
||||
const std::vector<unsigned int> selected_gradient_ids = decode_gradient_ids(m_mf.gradient_component_ids);
|
||||
const bool component_surface_offsets_supported = (m_pattern_ctrl == nullptr) &&
|
||||
const bool component_surface_offsets_supported = m_bias_mode_enabled &&
|
||||
(m_pattern_ctrl == nullptr) &&
|
||||
!same_layer_mode &&
|
||||
!m_preview_settings.local_z_mode;
|
||||
if (m_surface_offset_target_choice)
|
||||
m_surface_offset_target_choice->Enable(component_surface_offsets_supported);
|
||||
if (m_surface_offset_spin)
|
||||
m_surface_offset_spin->Enable(component_surface_offsets_supported);
|
||||
if (preview_sequence.empty())
|
||||
@@ -6311,11 +6367,11 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
m_blend_selector->set_multi_preview(corner_colors, *m_selected_weight_state);
|
||||
}
|
||||
|
||||
if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings) &&
|
||||
if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) &&
|
||||
m_mf.component_a >= 1 && m_mf.component_b >= 1 &&
|
||||
m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) {
|
||||
const auto [apparent_pct_a, apparent_pct_b] =
|
||||
mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters);
|
||||
mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled);
|
||||
m_mf.display_color = MixedFilamentManager::blend_color(
|
||||
m_physical_colors[size_t(m_mf.component_a - 1)],
|
||||
m_physical_colors[size_t(m_mf.component_b - 1)],
|
||||
@@ -6344,13 +6400,14 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
}
|
||||
|
||||
if (m_mix_preview) {
|
||||
const std::string bias_summary = mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters);
|
||||
const std::string bias_summary =
|
||||
mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled);
|
||||
const std::string summary = bias_summary.empty() ? summarize_sequence(preview_sequence) : bias_summary;
|
||||
std::vector<double> preview_surface_offsets(m_palette.size() + 1, 0.0);
|
||||
if (m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_a] = std::max(0.0, double(m_mf.component_a_surface_offset));
|
||||
if (m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_b] = std::max(0.0, double(m_mf.component_b_surface_offset));
|
||||
if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset);
|
||||
if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset);
|
||||
m_mix_preview->set_data(m_palette, preview_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color),
|
||||
_L("Preview"), summary.empty() ? wxString() : from_u8(summary));
|
||||
}
|
||||
@@ -6448,8 +6505,6 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
evt.Skip();
|
||||
});
|
||||
}
|
||||
if (m_surface_offset_target_choice)
|
||||
m_surface_offset_target_choice->Bind(wxEVT_CHOICE, [apply_changes](wxCommandEvent &) { apply_changes(); });
|
||||
if (m_surface_offset_spin) {
|
||||
m_surface_offset_spin->Bind(wxEVT_SPINCTRLDOUBLE, [apply_changes](wxSpinDoubleEvent &) { apply_changes(); });
|
||||
m_surface_offset_spin->Bind(wxEVT_TEXT_ENTER, [apply_changes](wxCommandEvent &) { apply_changes(); });
|
||||
@@ -6503,6 +6558,7 @@ void MixedFilamentConfigPanel::build_ui()
|
||||
}
|
||||
}
|
||||
|
||||
update_component_picker_visuals();
|
||||
SetSizer(root);
|
||||
Layout();
|
||||
SetMinSize(wxSize(-1, GetBestSize().GetHeight()));
|
||||
@@ -6557,22 +6613,33 @@ void MixedFilamentConfigPanel::update_component_picker_visuals()
|
||||
update_one(m_choice_c, m_picker_c_container, m_picker_c_swatch, m_picker_c_label);
|
||||
update_one(m_choice_d, m_picker_d_container, m_picker_d_swatch, m_picker_d_label);
|
||||
|
||||
if (m_surface_offset_target_choice) {
|
||||
if (m_surface_offset_target_container || m_surface_offset_target_swatch || m_surface_offset_target_label || m_surface_offset_spin) {
|
||||
const int a_filament = std::clamp(m_choice_a ? (m_choice_a->GetSelection() + 1) : int(m_mf.component_a), 1, int(std::max<size_t>(1, m_num_physical)));
|
||||
const int b_filament = std::clamp(m_choice_b ? (m_choice_b->GetSelection() + 1) : int(m_mf.component_b), 1, int(std::max<size_t>(1, m_num_physical)));
|
||||
if (m_surface_offset_target_choice->GetCount() >= 2) {
|
||||
m_surface_offset_target_choice->SetString(0, wxString::Format("F%d", a_filament));
|
||||
m_surface_offset_target_choice->SetString(1, wxString::Format("F%d", b_filament));
|
||||
MixedFilament active_pair = m_mf;
|
||||
active_pair.component_a = unsigned(a_filament);
|
||||
active_pair.component_b = unsigned(b_filament);
|
||||
double signed_bias_value = mixed_filament_single_surface_offset_value(active_pair, m_nozzle_diameters);
|
||||
|
||||
if (m_surface_offset_spin && m_bias_mode_enabled) {
|
||||
const double bias_limit = mixed_filament_bias_limit_mm(active_pair, m_nozzle_diameters);
|
||||
m_surface_offset_spin->SetRange(-bias_limit, bias_limit);
|
||||
signed_bias_value = m_surface_offset_spin->GetValue();
|
||||
}
|
||||
|
||||
const int selected_target = std::clamp(m_surface_offset_target_choice->GetSelection(), 0, 1);
|
||||
const int color_idx = (selected_target == 1 ? b_filament : a_filament) - 1;
|
||||
const int active_filament = signed_bias_value < -EPSILON ? a_filament : b_filament;
|
||||
const int color_idx = active_filament - 1;
|
||||
const wxColour color = (color_idx >= 0 && size_t(color_idx) < m_palette.size()) ? m_palette[size_t(color_idx)] : wxColour("#26A69A");
|
||||
if (m_surface_offset_target_swatch) {
|
||||
m_surface_offset_target_swatch->SetBackgroundColour(color);
|
||||
m_surface_offset_target_swatch->Refresh();
|
||||
}
|
||||
m_surface_offset_target_choice->Refresh();
|
||||
if (m_surface_offset_target_label)
|
||||
m_surface_offset_target_label->SetLabel(wxString::Format("F%d", active_filament));
|
||||
if (m_surface_offset_target_container) {
|
||||
m_surface_offset_target_container->Layout();
|
||||
m_surface_offset_target_container->Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6613,11 +6680,11 @@ void MixedFilamentConfigPanel::update_preview()
|
||||
}
|
||||
|
||||
if (m_mix_preview) {
|
||||
if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings) &&
|
||||
if (mixed_filament_supports_bias_apparent_color(m_mf, m_preview_settings, m_bias_mode_enabled) &&
|
||||
m_mf.component_a >= 1 && m_mf.component_b >= 1 &&
|
||||
m_mf.component_a <= m_physical_colors.size() && m_mf.component_b <= m_physical_colors.size()) {
|
||||
const auto [apparent_pct_a, apparent_pct_b] =
|
||||
mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters);
|
||||
mixed_filament_apparent_pair_percentages(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled);
|
||||
m_mf.display_color = MixedFilamentManager::blend_color(
|
||||
m_physical_colors[size_t(m_mf.component_a - 1)],
|
||||
m_physical_colors[size_t(m_mf.component_b - 1)],
|
||||
@@ -6625,13 +6692,14 @@ void MixedFilamentConfigPanel::update_preview()
|
||||
apparent_pct_b);
|
||||
}
|
||||
|
||||
const std::string bias_summary = mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters);
|
||||
const std::string bias_summary =
|
||||
mixed_filament_apparent_pair_summary(m_mf, m_preview_settings, m_nozzle_diameters, m_bias_mode_enabled);
|
||||
const std::string summary = bias_summary.empty() ? summarize_sequence(initial_sequence) : bias_summary;
|
||||
std::vector<double> preview_surface_offsets(m_palette.size() + 1, 0.0);
|
||||
if (m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_a] = std::max(0.0, double(m_mf.component_a_surface_offset));
|
||||
if (m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_b] = std::max(0.0, double(m_mf.component_b_surface_offset));
|
||||
if (m_bias_mode_enabled && m_mf.component_a >= 1 && m_mf.component_a < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_a] = double(m_mf.component_a_surface_offset);
|
||||
if (m_bias_mode_enabled && m_mf.component_b >= 1 && m_mf.component_b < preview_surface_offsets.size())
|
||||
preview_surface_offsets[m_mf.component_b] = double(m_mf.component_b_surface_offset);
|
||||
m_mix_preview->set_data(m_palette, initial_sequence, same_layer_mode, preview_surface_offsets, wxColour(m_mf.display_color),
|
||||
_L("Preview"), summary.empty() ? wxString() : from_u8(summary));
|
||||
}
|
||||
@@ -6870,6 +6938,22 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
|
||||
else
|
||||
preset_bundle->project_config.set_key_value(key, new ConfigOptionString(value));
|
||||
};
|
||||
auto set_mixed_bool = [preset_bundle, print_cfg](const std::string &key, bool value) {
|
||||
if (print_cfg) {
|
||||
if (ConfigOptionBool *opt = print_cfg->option<ConfigOptionBool>(key))
|
||||
opt->value = value;
|
||||
else if (ConfigOptionInt *opt = print_cfg->option<ConfigOptionInt>(key))
|
||||
opt->value = value ? 1 : 0;
|
||||
else
|
||||
print_cfg->set_key_value(key, new ConfigOptionBool(value));
|
||||
}
|
||||
if (ConfigOptionBool *opt = preset_bundle->project_config.option<ConfigOptionBool>(key))
|
||||
opt->value = value;
|
||||
else if (ConfigOptionInt *opt = preset_bundle->project_config.option<ConfigOptionInt>(key))
|
||||
opt->value = value ? 1 : 0;
|
||||
else
|
||||
preset_bundle->project_config.set_key_value(key, new ConfigOptionBool(value));
|
||||
};
|
||||
auto set_mixed_mode = [preset_bundle, print_cfg](bool enabled) {
|
||||
if (print_cfg) {
|
||||
if (ConfigOptionBool *opt = print_cfg->option<ConfigOptionBool>("mixed_filament_gradient_mode"))
|
||||
@@ -7088,6 +7172,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
|
||||
if (print_cfg && print_cfg->has("wall_loops"))
|
||||
wall_loops = std::max<size_t>(1, size_t(std::max(1, print_cfg->opt_int("wall_loops"))));
|
||||
const bool local_z_mode = get_mixed_bool("dithering_local_z_mode", false);
|
||||
const bool component_bias_enabled = get_mixed_bool("mixed_filament_component_bias_enabled", false);
|
||||
float pointillism_pixel_size = std::max(0.f, get_mixed_float("mixed_filament_pointillism_pixel_size", 0.f));
|
||||
float pointillism_line_gap = std::max(0.f, get_mixed_float("mixed_filament_pointillism_line_gap", 0.f));
|
||||
float mixed_surface_indentation = std::clamp(get_mixed_float("mixed_filament_surface_indentation", 0.f), -2.f, 2.f);
|
||||
@@ -7185,13 +7270,14 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
|
||||
const bool same_layer_mode = entry.distribution_mode == int(MixedFilament::SameLayerPointillisme);
|
||||
return build_effective_pair_preview_sequence(entry.component_a, entry.component_b, effective_mix_b, same_layer_mode);
|
||||
};
|
||||
auto compute_entry_display_color = [num_physical, &physical_colors, &nozzle_diameters, blend_from_sequence, build_entry_preview_sequence, preview_settings](const MixedFilament &entry) {
|
||||
if (mixed_filament_supports_bias_apparent_color(entry, preview_settings) &&
|
||||
auto compute_entry_display_color = [num_physical, &physical_colors, &nozzle_diameters, blend_from_sequence, build_entry_preview_sequence,
|
||||
preview_settings, component_bias_enabled](const MixedFilament &entry) {
|
||||
if (mixed_filament_supports_bias_apparent_color(entry, preview_settings, component_bias_enabled) &&
|
||||
entry.component_a >= 1 && entry.component_b >= 1 &&
|
||||
entry.component_a <= num_physical && entry.component_b <= num_physical &&
|
||||
entry.component_a <= physical_colors.size() && entry.component_b <= physical_colors.size()) {
|
||||
const auto [apparent_pct_a, apparent_pct_b] =
|
||||
mixed_filament_apparent_pair_percentages(entry, preview_settings, nozzle_diameters);
|
||||
mixed_filament_apparent_pair_percentages(entry, preview_settings, nozzle_diameters, component_bias_enabled);
|
||||
return MixedFilamentManager::blend_color(
|
||||
physical_colors[entry.component_a - 1],
|
||||
physical_colors[entry.component_b - 1],
|
||||
@@ -7225,11 +7311,21 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
|
||||
mixed_mgr.apply_gradient_settings(gradient_mode, lower_bound, upper_bound, advanced_dithering);
|
||||
}
|
||||
|
||||
if (component_bias_enabled) {
|
||||
for (MixedFilament &entry : mixed_mgr.mixed_filaments()) {
|
||||
const float bias_value = mixed_filament_single_surface_offset_value(entry, nozzle_diameters);
|
||||
const auto balanced_pair = mixed_filament_single_surface_offset_pair(entry, bias_value, nozzle_diameters);
|
||||
entry.component_a_surface_offset = balanced_pair.first;
|
||||
entry.component_b_surface_offset = balanced_pair.second;
|
||||
}
|
||||
}
|
||||
|
||||
// During project load, sidebar may refresh before physical filament combos
|
||||
// finish syncing. Avoid overwriting persisted mixed definitions while the
|
||||
// physical filament set is incomplete.
|
||||
if (num_physical >= 2) {
|
||||
set_mixed_mode(height_weighted_mode);
|
||||
set_mixed_bool("mixed_filament_component_bias_enabled", component_bias_enabled);
|
||||
set_mixed_float("mixed_filament_height_lower_bound", lower_bound);
|
||||
set_mixed_float("mixed_filament_height_upper_bound", upper_bound);
|
||||
set_mixed_float("mixed_color_layer_height_a", preferred_local_z_a);
|
||||
@@ -7640,7 +7736,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
|
||||
return row->GetClientRect().Contains(local);
|
||||
};
|
||||
|
||||
auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, preset_bundle,
|
||||
auto ensure_editor = [this, mixed_id, num_physical, physical_colors, nozzle_diameters, palette, preview_settings, component_bias_enabled, preset_bundle,
|
||||
editor_host, editor_sizer, swatch, summary_label, header_panel, row,
|
||||
rows_scroller, mixed_summary_text, apply_mixed_entry_changes]() {
|
||||
if (!preset_bundle || !editor_sizer || editor_sizer->GetItemCount() > 0)
|
||||
@@ -7652,6 +7748,7 @@ void Sidebar::update_mixed_filament_panel(bool sync_manager)
|
||||
return;
|
||||
|
||||
auto *editor = new MixedFilamentConfigPanel(editor_host, mixed_id, mfs[mixed_id], num_physical, physical_colors, nozzle_diameters, palette, preview_settings,
|
||||
component_bias_enabled,
|
||||
[this, mixed_id, swatch, summary_label, header_panel, row, rows_scroller, mixed_summary_text, apply_mixed_entry_changes](const MixedFilament &updated_mf) {
|
||||
apply_mixed_entry_changes(mixed_id, updated_mf, true);
|
||||
|
||||
@@ -10360,6 +10457,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
"mixed_filament_advanced_dithering",
|
||||
"mixed_filament_pointillism_pixel_size",
|
||||
"mixed_filament_pointillism_line_gap",
|
||||
"mixed_filament_component_bias_enabled",
|
||||
"mixed_filament_surface_indentation"
|
||||
};
|
||||
preset_bundle->project_config.apply_only(config_loaded, imported_project_option_keys, true);
|
||||
|
||||
@@ -1793,6 +1793,9 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
return;
|
||||
}
|
||||
|
||||
const bool refresh_mixed_filament_panel =
|
||||
m_type == Preset::TYPE_PRINT && opt_key == "mixed_filament_component_bias_enabled";
|
||||
|
||||
// Keep Mixed Filaments global settings in sync with project_config. In
|
||||
// full_fff_config(), project_config is applied last and would otherwise
|
||||
// override the edited print preset value from the Others panel.
|
||||
@@ -1805,6 +1808,7 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
opt_key == "mixed_filament_advanced_dithering" ||
|
||||
opt_key == "mixed_filament_pointillism_pixel_size" ||
|
||||
opt_key == "mixed_filament_pointillism_line_gap" ||
|
||||
opt_key == "mixed_filament_component_bias_enabled" ||
|
||||
opt_key == "mixed_filament_surface_indentation" ||
|
||||
opt_key == "mixed_filament_region_collapse" ||
|
||||
opt_key == "dithering_z_step_size" ||
|
||||
@@ -1820,6 +1824,9 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
if(m_active_page)
|
||||
m_active_page->update_visibility(m_mode, true);
|
||||
m_page_view->GetParent()->Layout();
|
||||
|
||||
if (refresh_mixed_filament_panel && wxGetApp().plater() != nullptr)
|
||||
wxGetApp().sidebar().update_mixed_filament_panel(false);
|
||||
}
|
||||
|
||||
void Tab::show_timelapse_warning_dialog() {
|
||||
@@ -2543,6 +2550,7 @@ optgroup->append_single_option_line("skirt_loops", "others_settings_skirt#loops"
|
||||
optgroup->append_single_option_line("mixed_filament_advanced_dithering");
|
||||
optgroup->append_single_option_line("mixed_filament_pointillism_pixel_size");
|
||||
optgroup->append_single_option_line("mixed_filament_pointillism_line_gap");
|
||||
optgroup->append_single_option_line("mixed_filament_component_bias_enabled");
|
||||
optgroup->append_single_option_line("mixed_filament_surface_indentation");
|
||||
optgroup->append_single_option_line("mixed_filament_region_collapse");
|
||||
optgroup->append_single_option_line("dithering_z_step_size");
|
||||
|
||||
@@ -124,7 +124,7 @@ TEST_CASE("Mixed filament remap keeps later painted colors stable when an earlie
|
||||
bundle.update_multi_material_filament_presets();
|
||||
|
||||
auto &mixed = bundle.mixed_filaments.mixed_filaments();
|
||||
REQUIRE(mixed.size() >= 10);
|
||||
REQUIRE(mixed.size() >= 6);
|
||||
|
||||
const uint64_t stable_id_6 = mixed[1].stable_id;
|
||||
const uint64_t stable_id_7 = mixed[2].stable_id;
|
||||
@@ -137,7 +137,7 @@ TEST_CASE("Mixed filament remap keeps later painted colors stable when an earlie
|
||||
bundle.update_mixed_filament_id_remap(old_mixed, 4, 4);
|
||||
const std::vector<unsigned int> remap = bundle.consume_last_filament_id_remap();
|
||||
|
||||
REQUIRE(remap.size() >= 15);
|
||||
REQUIRE(remap.size() >= 11);
|
||||
CHECK(remap[6] == virtual_id_for_stable_id(mixed, 4, stable_id_6));
|
||||
CHECK(remap[7] == virtual_id_for_stable_id(mixed, 4, stable_id_7));
|
||||
CHECK(remap[8] == virtual_id_for_stable_id(mixed, 4, stable_id_8));
|
||||
@@ -164,7 +164,7 @@ TEST_CASE("Mixed filament grouped manual patterns normalize and round-trip", "[M
|
||||
CHECK(loaded.mixed_filaments().front().mix_b_percent == 13);
|
||||
}
|
||||
|
||||
TEST_CASE("Mixed filament component surface offsets round-trip and follow the active layer component", "[MixedFilament]")
|
||||
TEST_CASE("Mixed filament component surface offsets round-trip and bias the second layer component", "[MixedFilament]")
|
||||
{
|
||||
const std::vector<std::string> colors = {"#FF0000", "#FFFF00"};
|
||||
|
||||
@@ -189,18 +189,98 @@ TEST_CASE("Mixed filament component surface offsets round-trip and follow the ac
|
||||
const MixedFilament &loaded_row = loaded.mixed_filaments().front();
|
||||
CHECK(loaded_row.component_a_surface_offset == Approx(0.02f));
|
||||
CHECK(loaded_row.component_b_surface_offset == Approx(-0.01f));
|
||||
CHECK(loaded.component_surface_offset(3, 2, 0) == Approx(0.02f));
|
||||
CHECK(loaded.component_surface_offset(3, 2, 1) == Approx(-0.01f));
|
||||
CHECK(loaded.component_surface_offset(3, 2, 0) == Approx(0.01f));
|
||||
CHECK(loaded.component_surface_offset(3, 2, 1) == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("Mixed filament apparent mix percent shifts toward the less-contracted component", "[MixedFilament]")
|
||||
TEST_CASE("Mixed filament apparent mix percent follows the signed bias target", "[MixedFilament]")
|
||||
{
|
||||
CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.02f, 0.00f, 0.4f) == 55);
|
||||
CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.00f, 0.00f, 0.4f) == 50);
|
||||
CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.00f, 0.02f, 0.4f) == 45);
|
||||
CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.02f, 0.00f, 0.4f) == 55);
|
||||
CHECK(MixedFilamentManager::apparent_mix_b_percent(50, -0.02f, 0.00f, 0.4f) == 45);
|
||||
CHECK(MixedFilamentManager::apparent_mix_b_percent(50, 0.00f, -0.02f, 0.4f) == 55);
|
||||
}
|
||||
|
||||
TEST_CASE("Mixed filament bias helper maps signed bias to a one-sided safe offset pair", "[MixedFilament]")
|
||||
{
|
||||
const auto [offset_a, offset_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.06f, 0.4f);
|
||||
CHECK(offset_a == Approx(0.0f));
|
||||
CHECK(offset_b == Approx(0.06f));
|
||||
|
||||
CHECK(MixedFilamentManager::bias_ui_value_from_surface_offsets(offset_a, offset_b, 0.4f) == Approx(0.06f));
|
||||
|
||||
CHECK(MixedFilamentManager::bias_ui_value_from_surface_offsets(0.02f, 0.0f, 0.4f) == Approx(-0.02f));
|
||||
CHECK(MixedFilamentManager::bias_ui_value_from_surface_offsets(-0.02f, 0.0f, 0.4f) == Approx(0.02f));
|
||||
|
||||
const auto [negative_a, negative_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.06f, 0.4f);
|
||||
CHECK(negative_a == Approx(0.06f));
|
||||
CHECK(negative_b == Approx(0.0f));
|
||||
|
||||
const auto [unclamped_a, unclamped_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.30f, 0.4f);
|
||||
CHECK(unclamped_a == Approx(0.0f));
|
||||
CHECK(unclamped_b == Approx(0.30f));
|
||||
|
||||
const auto [unclamped_negative_a, unclamped_negative_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.30f, 0.4f);
|
||||
CHECK(unclamped_negative_a == Approx(0.30f));
|
||||
CHECK(unclamped_negative_b == Approx(0.0f));
|
||||
|
||||
const auto [clamped_a, clamped_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.40f, 0.4f);
|
||||
CHECK(clamped_a == Approx(0.0f));
|
||||
CHECK(clamped_b == Approx(0.35f));
|
||||
|
||||
const auto [clamped_negative_a, clamped_negative_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.40f, 0.4f);
|
||||
CHECK(clamped_negative_a == Approx(0.35f));
|
||||
CHECK(clamped_negative_b == Approx(0.0f));
|
||||
}
|
||||
|
||||
TEST_CASE("Mixed filament component surface offsets follow the signed bias target across alternating layers", "[MixedFilament]")
|
||||
{
|
||||
const std::vector<std::string> colors = {"#FF0000", "#FFFF00"};
|
||||
|
||||
MixedFilamentManager mgr;
|
||||
mgr.add_custom_filament(1, 2, 50, colors);
|
||||
REQUIRE(mgr.mixed_filaments().size() == 1);
|
||||
|
||||
MixedFilament &row = mgr.mixed_filaments().front();
|
||||
row.manual_pattern.clear();
|
||||
row.distribution_mode = int(MixedFilament::Simple);
|
||||
row.ratio_a = 1;
|
||||
row.ratio_b = 1;
|
||||
|
||||
{
|
||||
const auto [offset_a, offset_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(0.05f, 0.4f);
|
||||
row.component_a_surface_offset = offset_a;
|
||||
row.component_b_surface_offset = offset_b;
|
||||
|
||||
CHECK(mgr.component_surface_offset(3, 2, 0) == Approx(0.0f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 1) == Approx(0.05f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 2) == Approx(0.0f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 3) == Approx(0.05f));
|
||||
}
|
||||
|
||||
{
|
||||
row.component_a_surface_offset = 0.05f;
|
||||
row.component_b_surface_offset = 0.0f;
|
||||
|
||||
CHECK(mgr.component_surface_offset(3, 2, 0) == Approx(0.05f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 1) == Approx(0.0f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 2) == Approx(0.05f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 3) == Approx(0.0f));
|
||||
}
|
||||
|
||||
{
|
||||
const auto [offset_a, offset_b] = MixedFilamentManager::surface_offset_pair_from_signed_bias(-0.05f, 0.4f);
|
||||
row.component_a_surface_offset = offset_a;
|
||||
row.component_b_surface_offset = offset_b;
|
||||
|
||||
CHECK(mgr.component_surface_offset(3, 2, 0) == Approx(0.05f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 1) == Approx(0.0f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 2) == Approx(0.05f));
|
||||
CHECK(mgr.component_surface_offset(3, 2, 3) == Approx(0.0f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Mixed filament auto generation can be disabled without dropping custom rows", "[MixedFilament]")
|
||||
{
|
||||
const std::vector<std::string> colors = {"#FF0000", "#00FF00", "#0000FF"};
|
||||
|
||||
+3
-3
@@ -10,8 +10,8 @@ endif()
|
||||
if(NOT DEFINED BBL_INTERNAL_TESTING)
|
||||
set(BBL_INTERNAL_TESTING "0")
|
||||
endif()
|
||||
set(Snapmaker_VERSION "0.9.5")
|
||||
set(FULLSPECTRUM_VERSION "0.9.5")
|
||||
set(Snapmaker_VERSION "0.9.51")
|
||||
set(FULLSPECTRUM_VERSION "0.9.51")
|
||||
set(MIN_FIRM_VER "1.0.0")
|
||||
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)"
|
||||
Snapmaker_VERSION_MATCH ${Snapmaker_VERSION})
|
||||
@@ -19,7 +19,7 @@ set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1})
|
||||
set(ORCA_VERSION_MINOR ${CMAKE_MATCH_2})
|
||||
set(ORCA_VERSION_PATCH ${CMAKE_MATCH_3})
|
||||
|
||||
set(SLIC3R_VERSION "01.10.01.50")
|
||||
set(SLIC3R_VERSION "01.10.01.51")
|
||||
|
||||
if (NOT DEFINED ORCA_UPDATER_SIG_KEY)
|
||||
set(ORCA_UPDATER_SIG_KEY "" CACHE STRING "Base64url encoded updater signature key")
|
||||
|
||||
Reference in New Issue
Block a user