init work to integrate OrcaSlicer-FullSpectrum fork Integrations up to commit b3c41fda41.

- libslic3r: vendor FilamentMixer; MixedFilamentManager (auto-gen, resolve,
    serialize; manual-pattern / gradient / pointillism); 19 new PrintConfig
    keys; PresetBundle owns the canonical manager with 3MF + AppConfig
    roundtrip and AMS-safe strip+restore; Print owns the slicing-time copy
    with PrintApply auto-regen on color change; TriangleSelector::
    shift_states_above + filament-id remap; inset_idx propagation through
    ExtrusionPath/Loop/MultiPath copy/assign.
  - Slicing: virtual filament IDs in painted regions (same-physical channels
    collapse when mixed_filament_region_collapse is on); ByObject
    collect_filament_data expands mixed slots; pair-cadence + whole-object +
    3+component Local-Z plan generators; LocalZOrderOptimizer utility.
  - GCode + ToolOrdering: LayerTools resolves virtual IDs through wall /
    infill / sparse / solid queries; SameLayerPointillisme in process_layer
    (uniform-segment + grouped per-perimeter-index splitters); WipeTower2
    Local-Z reservation + sub-layer G-code emission; per-layer infill
    filament override.
  - PartPlate: get_extruders* expand virtual slots into physical components;
    CLI path rebuilds a local manager from full_config.
  - GUI: five widget files extracted from FS Plater.cpp (~5000 LOC) —
    MixedMixPreview, MixedGradientSelector + WeightsDialog,
    MixedFilamentColorMapPanel, MixedFilamentColorMatchDialog (ΔE₀₀ recipe
    search), MixedFilamentConfigPanel; Sidebar Mixed Filaments panel
    (drag-reorder, enable/delete, Add Gradient/Pattern/Color); Tab exposure
    of mixed-filament / dithering / per-layer infill-override settings +
    ConfigManipulation visibility and slot-validation rules;
    BBLMixedFilamentBroken / BBLSingleExtruderMixedFilamentRisk
    notifications + slice gate; WipeTowerDialog edits physical P×P
    sub-matrix; bounds-safe extruder_id guards in 3DScene / GLCanvas3D /
    GLGizmoMmuSegmentation; change_filament merge guard and
    on_filaments_delete is_mixed_before_delete propagation.
  - Tests: 4 Catch2 tests for 3MF roundtrip (auto/custom persistence,
    PresetBundle string path, total_filaments stability); full-pipeline
    slice E2E deferred — TODO in file.

Co-authored-by: Rad <radugheorghiu96@gmail.com>
Co-authored-by: Justin Hayes <justinh@rahb.ca>
Co-authored-by: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-authored-by: xSil3nt <ahmedshazin21@gmail.com>
Co-authored-by: ratdoux <62392831+ratdoux@users.noreply.github.com>

Update TriangleSelector.cpp

Co-Authored-By: Rad <radugheorghiu96@gmail.com>
Co-Authored-By: Justin Hayes <justinh@rahb.ca>
Co-Authored-By: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-Authored-By: xSil3nt <ahmedshazin21@gmail.com>
Co-Authored-By: ratdoux <62392831+ratdoux@users.noreply.github.com>
This commit is contained in:
SoftFever
2026-06-01 09:24:53 -03:00
committed by Ian Bassi
co-authored by Rad Justin Hayes Calogero Guagenti xSil3nt ratdoux
parent 7a0c149701
commit 0e0e34c8b4
68 changed files with 18988 additions and 267 deletions
+171
View File
@@ -0,0 +1,171 @@
# Mixed Filament
Ported from [OrcaSlicer-FullSpectrum](https://github.com/SoftFever/OrcaSlicer-FullSpectrum)
with contributions from Rad, Justin Hayes, Calogero Guagenti, xSil3nt, and ratdoux.
---
## User Guide
### What It Does
Mixed Filament lets a single virtual filament slot alternate between two
physical filaments across layers (or within a layer in Pointillisme mode),
producing blended or gradient-like colours on single-extruder printers.
### Enabling
1. Load a multi-colour (multi-extruder) profile with at least 2 filaments.
2. The **Mixed Filaments** panel appears automatically in the right-hand sidebar
when 2 or more filaments are configured.
3. Each auto-generated row represents one pair of physical filaments.
Toggle a row to enable it; the total filament count grows to include the
virtual slot.
### Sidebar Workflow
- **Add** — creates a custom row for the same pair or a different ratio.
- **Edit** — opens `MixedFilamentConfigPanel` to adjust ratio, pattern,
surface offset (bias), and distribution mode.
- **Delete** — marks the row deleted; existing painted geometry retains its
virtual filament ID until you re-slice or repaint.
Painting with a virtual filament ID behaves the same as painting with a
physical one — use the Multi-Material Painting gizmo and select the virtual
slot from the colour palette.
### Color Match Dialog
Open via the colour swatch on a mixed row. The dialog (`MixedFilamentColorMatchDialog`)
shows a live preview strip of the blended result and lets you adjust ratio
until the preview matches your target colour. The preview accounts for
surface-offset bias when enabled.
### Anti-Banding Options
| Setting | What it does |
|---|---|
| `mixed_filament_advanced_dithering` | Uses an ordered dither pattern instead of simple A-then-B runs. Reduces stripe visibility on some hue pairs. More experimental than the default. |
| `dithering_local_z_mode` | Splits each blended layer into two sub-layers whose heights are proportional to the mix ratio (e.g. 66/33 at 0.12 mm → 0.08 mm + 0.04 mm). Produces the smoothest colour gradients. |
| `dithering_local_z_whole_objects` | Extends Local-Z splitting beyond painted masks to cover the entire object cross-section. Useful when mixed walls surround a painted zone. |
| `dithering_local_z_direct_multicolor` | For rows with 3 or more physical components, allocates Local-Z sub-layers directly across all components with carry-over error correction instead of collapsing to pair cadence. More toolchanges; less banding. |
### Gotchas
- **Single-extruder warning** — Mixed Filament requires a physical toolchange
between the two components. On a true single-nozzle printer this means a
manual filament swap. Verify your printer profile supports `T0`/`T1` before
using mixed slots in a production print.
- **Variable-layer interaction** — If Variable Layer Height is enabled, Local-Z
sub-layer heights are recomputed per interval. The mix ratio is preserved but
the absolute sub-layer heights change with the variable height. Review the
layer preview after applying variable layers.
- **Custom sequence disabled** — OrcaSlicer's "custom toolchange sequence" is
suppressed when mixed filaments are active (`PlateSettingsDialog`). The
virtual-to-physical resolution must control toolchange order; a user-defined
sequence would break it.
- **Stable IDs** — each mixed row carries a `stable_id` (64-bit). If you
reorder or delete rows and then load an older project, the ID remap in
`PresetBundle::update_mixed_filament_id_remap` translates painted geometry
to the correct new virtual slot. Do not rely on the 1-based filament index
as a stable identifier.
---
## Developer Guide
### Core Data Structures
```
src/libslic3r/MixedFilament.hpp — MixedFilament struct, MixedFilamentManager
src/libslic3r/MixedFilament.cpp — serialization, resolve(), auto_generate()
src/libslic3r/LocalZOrderOptimizer.hpp — bucket-ordering helpers for Local-Z
```
The key scalar fields on `MixedFilament`:
- `component_a`, `component_b` — 1-based physical filament indices.
- `ratio_a`, `ratio_b` — layer-alternation cadence numerators.
- `mix_b_percent` — nominal colour mix (used for Local-Z height computation
and the Color Match preview; does not change the cadence).
- `stable_id` — monotonically increasing 64-bit ID assigned at construction.
Never reused. Survives serialization round-trips.
- `distribution_mode` — selects between `Simple`, `SameLayerPointillisme`,
and `GroupedManual`.
### Seam: Adding New Distribution Modes
`MixedFilamentManager::resolve()` in `MixedFilament.cpp` is the single
dispatch point that maps `(virtual_filament_id, num_physical, layer_index)`
to a physical extruder. The current switch covers `Simple` and
`SameLayerPointillisme`. A new mode is added by:
1. Adding a value to the `MixedFilament::DistributionMode` enum in
`MixedFilament.hpp`.
2. Adding a `case` to `MixedFilamentManager::resolve()` in `MixedFilament.cpp`.
3. Serializing the new mode token in `serialize_custom_entries` /
`load_custom_entries` (format is a semicolon-delimited row string; see
existing tokens for the convention).
G-code emission (`src/libslic3r/GCode/`) reads only the physical ID returned
by `resolve()`, so new modes are automatically emitted without further changes.
### Seam: New Toolchange-Cost Heuristics
`LocalZOrderOptimizer` (`src/libslic3r/LocalZOrderOptimizer.hpp`) exposes:
- `order_bucket_extruders(bucket, current, preferred_last)` — reorders a
single-layer bucket to minimise toolchanges given the current active extruder.
- `order_pass_group(group, current_extruder)` — greedy walk across a set of
buckets (one per Local-Z sub-layer) to minimise total transitions.
To add a new heuristic (e.g. cost-based look-ahead), replace or wrap
`order_pass_group`. The caller in `PrintObjectSlice.cpp` passes the result
directly into the sub-layer plan, so the heuristic is fully decoupled from
the plan builder.
### Seam: New Picker Shapes in the Color Map Panel
`MixedFilamentColorMapPanel` (`src/slic3r/GUI/MixedFilamentColorMapPanel.hpp`)
renders a 2-D colour map using a set of geometry "types" (currently strip and
gradient). Each type is a small self-contained rendering path keyed by an enum
value. New shapes are added by:
1. Adding an enum value to `MixedFilamentColorMapPanel::GeometryType`.
2. Implementing the corresponding `Paint*` helper (follow `PaintStrip` as a
template).
3. Wiring the new type into the `switch` in `OnPaint`.
### Persistence (3MF)
The entire mixed-filament state is stored as a single string key
`mixed_filament_definitions` in the project config block (section `[presets]`
in the 3MF metadata).
Round-trip path:
```
MixedFilamentManager::serialize_custom_entries()
called by PresetBundle::sync_mixed_filaments_to_config()
written by bbs_3mf: store_bbs_3mf → config.set("presets", "mixed_filament_definitions", ...)
load_bbs_3mf → config.get("presets", "mixed_filament_definitions")
stored in project_config["mixed_filament_definitions"]
read by PresetBundle::sync_mixed_filaments_from_config()
→ mixed_filaments.auto_generate(colours)
→ mixed_filaments.load_custom_entries(defs, colours)
```
Auto-generated rows are *not* written to the definitions string; they are
rebuilt from the filament colour list. Only `custom == true` rows are stored.
See `tests/fff_print/test_mixed_filament_e2e.cpp` for regression tests
covering this path.
### ID Remap
When filaments are added, removed, or reordered, virtual IDs shift.
`PresetBundle::update_mixed_filament_id_remap(old_mixed, old_count, new_count)`
produces a `remap` vector where `remap[old_virtual_id] = new_virtual_id`.
Painted triangle mesh face data uses these IDs; the remap is applied in
`TriangleSelectorMixed` after any filament list change.
+4
View File
@@ -250,3 +250,7 @@ src/slic3r/GUI/RammingChart.cpp
src/slic3r/GUI/StepMeshDialog.cpp
src/slic3r/GUI/FilamentPickerDialog.hpp
src/libslic3r/PresetBundle.cpp
src/slic3r/GUI/MixedFilamentColorMatchDialog.cpp
src/slic3r/GUI/MixedFilamentColorMatchDialog.hpp
src/slic3r/GUI/MixedFilamentConfigPanel.cpp
src/slic3r/GUI/MixedFilamentConfigPanel.hpp
+4
View File
@@ -408,6 +408,10 @@ void AppConfig::set_defaults()
set("auto_calculate_flush","all");
}
if (get("auto_generate_gradients").empty()) {
set_bool("auto_generate_gradients", true);
}
if (get("show_canvas_zoom_button").empty()) {
set_bool("show_canvas_zoom_button", true);
}
+6
View File
@@ -176,6 +176,9 @@ set(lisbslic3r_sources
Fill/Lightning/Layer.hpp
Fill/Lightning/TreeNode.cpp
Fill/Lightning/TreeNode.hpp
filament_mixer.cpp
filament_mixer.h
filament_mixer_model.h
Flow.cpp
Flow.hpp
FlushVolCalc.cpp
@@ -286,6 +289,7 @@ set(lisbslic3r_sources
Line.hpp
LocalesUtils.cpp
LocalesUtils.hpp
LocalZOrderOptimizer.hpp
MarchingSquares.hpp
Measure.cpp
Measure.hpp
@@ -295,6 +299,8 @@ set(lisbslic3r_sources
MinAreaBoundingBox.hpp
MinimumSpanningTree.cpp
MinimumSpanningTree.hpp
MixedFilament.cpp
MixedFilament.hpp
miniz_extension.cpp
miniz_extension.hpp
ModelArrange.cpp
+60 -20
View File
@@ -170,7 +170,8 @@ public:
ExtrusionPath(ExtrusionRole role, double mm3_per_mm, float width, float height, bool no_extrusion = false) : mm3_per_mm(mm3_per_mm), width(width), height(height), m_role(role), m_no_extrusion(no_extrusion) {}
ExtrusionPath(const ExtrusionPath &rhs)
: polyline(rhs.polyline)
: ExtrusionEntity(rhs)
, polyline(rhs.polyline)
, overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm)
@@ -181,9 +182,12 @@ public:
, m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion)
{}
{
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath(ExtrusionPath &&rhs)
: polyline(std::move(rhs.polyline))
: ExtrusionEntity(rhs)
, polyline(std::move(rhs.polyline))
, overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm)
@@ -194,9 +198,12 @@ public:
, m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion)
{}
{
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath(const Polyline3 &polyline, const ExtrusionPath &rhs)
: polyline(polyline)
: ExtrusionEntity(rhs)
, polyline(polyline)
, overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm)
@@ -207,9 +214,12 @@ public:
, m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion)
{}
{
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath(Polyline3 &&polyline, const ExtrusionPath &rhs)
: polyline(std::move(polyline))
: ExtrusionEntity(rhs)
, polyline(std::move(polyline))
, overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm)
@@ -220,7 +230,9 @@ public:
, m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion)
{}
{
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath& operator=(const ExtrusionPath& rhs) {
m_can_reverse = rhs.m_can_reverse;
@@ -234,6 +246,7 @@ public:
this->overhang_degree = rhs.overhang_degree;
this->curve_degree = rhs.curve_degree;
this->polyline = rhs.polyline;
this->inset_idx = rhs.inset_idx;
return *this;
}
ExtrusionPath& operator=(ExtrusionPath&& rhs) {
@@ -248,6 +261,7 @@ public:
this->overhang_degree = rhs.overhang_degree;
this->curve_degree = rhs.curve_degree;
this->polyline = std::move(rhs.polyline);
this->inset_idx = rhs.inset_idx;
return *this;
}
@@ -380,21 +394,32 @@ public:
ExtrusionPaths paths;
ExtrusionMultiPath() {}
ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) {}
ExtrusionMultiPath(ExtrusionMultiPath &&rhs) : paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) {}
ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths) {}
ExtrusionMultiPath(const ExtrusionPath &path) {this->paths.push_back(path); m_can_reverse = path.can_reverse(); }
ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : ExtrusionEntity(rhs), paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) {}
ExtrusionMultiPath(ExtrusionMultiPath &&rhs) : ExtrusionEntity(rhs), paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) {}
ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths)
{
if (!paths.empty())
this->inset_idx = paths.front().inset_idx;
}
ExtrusionMultiPath(const ExtrusionPath &path)
{
this->paths.push_back(path);
this->inset_idx = path.inset_idx;
m_can_reverse = path.can_reverse();
}
ExtrusionMultiPath &operator=(const ExtrusionMultiPath &rhs)
{
this->paths = rhs.paths;
m_can_reverse = rhs.m_can_reverse;
this->paths = rhs.paths;
this->inset_idx = rhs.inset_idx;
m_can_reverse = rhs.m_can_reverse;
return *this;
}
ExtrusionMultiPath &operator=(ExtrusionMultiPath &&rhs)
{
this->paths = std::move(rhs.paths);
m_can_reverse = rhs.m_can_reverse;
this->paths = std::move(rhs.paths);
this->inset_idx = rhs.inset_idx;
m_can_reverse = rhs.m_can_reverse;
return *this;
}
@@ -447,12 +472,27 @@ public:
ExtrusionPaths paths;
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role) {}
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role)
{
if (!paths.empty())
this->inset_idx = paths.front().inset_idx;
}
ExtrusionLoop(ExtrusionPaths &&paths, ExtrusionLoopRole role = elrDefault) : paths(std::move(paths)), m_loop_role(role)
{
if (!this->paths.empty())
this->inset_idx = this->paths.front().inset_idx;
}
ExtrusionLoop(const ExtrusionPath &path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role)
{ this->paths.push_back(path); }
{
this->paths.push_back(path);
this->inset_idx = path.inset_idx;
}
ExtrusionLoop(const ExtrusionPath &&path, ExtrusionLoopRole role = elrDefault) : m_loop_role(role)
{ this->paths.emplace_back(std::move(path)); }
{
this->paths.emplace_back(std::move(path));
if (!this->paths.empty())
this->inset_idx = this->paths.front().inset_idx;
}
bool is_loop() const override{ return true; }
bool can_reverse() const override { return false; }
ExtrusionEntity* clone() const override{ return new ExtrusionLoop (*this); }
+27 -1
View File
@@ -11,6 +11,7 @@
#include "../Time.hpp"
#include "../I18N.hpp"
#include "../MixedFilament.hpp"
#include "bbs_3mf.hpp"
@@ -2223,7 +2224,32 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
const ConfigOptionStrings* filament_ids_opt = config.option<ConfigOptionStrings>("filament_settings_id");
int max_filament_id = filament_ids_opt ? filament_ids_opt->size() : std::numeric_limits<int>::max();
size_t physical_count = filament_ids_opt ? filament_ids_opt->size() : 0;
size_t max_filament_id_sz = physical_count;
if (filament_ids_opt != nullptr) {
const ConfigOptionString* mixed_opt = config.option<ConfigOptionString>("mixed_filament_definitions");
if (mixed_opt != nullptr && !mixed_opt->value.empty() && physical_count >= 2) {
std::vector<std::string> physical_colors;
if (const auto* colour_opt = config.option<ConfigOptionStrings>("filament_colour"))
physical_colors = colour_opt->values;
else if (const auto* default_colour_opt = config.option<ConfigOptionStrings>("default_filament_colour"))
physical_colors = default_colour_opt->values;
if (physical_colors.size() < physical_count)
physical_colors.resize(physical_count, "#FFFFFF");
else if (physical_colors.size() > physical_count)
physical_colors.resize(physical_count);
MixedFilamentManager mixed_mgr;
mixed_mgr.auto_generate(physical_colors);
mixed_mgr.load_custom_entries(mixed_opt->value, physical_colors);
max_filament_id_sz = mixed_mgr.total_filaments(physical_count);
}
} else {
max_filament_id_sz = size_t(std::numeric_limits<int>::max());
}
const int max_filament_id = max_filament_id_sz >= size_t(std::numeric_limits<int>::max())
? std::numeric_limits<int>::max()
: int(max_filament_id_sz);
for (ModelObject* mo : m_model->objects) {
const ConfigOptionInt* extruder_opt = dynamic_cast<const ConfigOptionInt*>(mo->config.option("extruder"));
int extruder_id = 0;
+1820 -12
View File
File diff suppressed because it is too large Load Diff
+44 -4
View File
@@ -81,17 +81,23 @@ public:
const Vec3d plate_origin,
const std::vector<WipeTower::ToolChangeResult> &priming,
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
const std::vector<std::vector<WipeTower::ToolChangeResult>> &local_z_tool_changes,
const WipeTower::ToolChangeResult &final_purge,
const std::vector<unsigned int> &slice_used_filaments) :
const std::vector<unsigned int> &slice_used_filaments,
const std::vector<std::vector<WipeTower::box_coordinates>> &local_z_reserve_boxes = {}) :
m_left(/*float(print_config.wipe_tower_x.value)*/ 0.f),
m_right(float(/*print_config.wipe_tower_x.value +*/ print_config.prime_tower_width.value)),
m_wipe_tower_pos(float(print_config.wipe_tower_x.get_at(plate_idx)), float(print_config.wipe_tower_y.get_at(plate_idx))),
m_wipe_tower_rotation(float(print_config.wipe_tower_rotation_angle)),
m_priming(priming),
m_tool_changes(tool_changes),
m_local_z_tool_changes(local_z_tool_changes),
m_local_z_reserve_boxes(local_z_reserve_boxes),
m_final_purge(final_purge),
m_layer_idx(-1),
m_tool_change_idx(0),
m_local_z_tool_change_idx(local_z_tool_changes.size(), 0),
m_local_z_reserve_slot_idx(local_z_reserve_boxes.size(), 0),
m_plate_origin(plate_origin),
m_single_extruder_multi_material(print_config.single_extruder_multi_material),
m_enable_timelapse_print(print_config.timelapse_type.value == TimelapseType::tlSmooth),
@@ -108,8 +114,18 @@ public:
}
std::string prime(GCode &gcodegen);
void next_layer() { ++ m_layer_idx; m_tool_change_idx = 0; }
std::string tool_change(GCode &gcodegen, int extruder_id, bool finish_layer);
void next_layer() {
++ m_layer_idx;
m_tool_change_idx = 0;
if (m_layer_idx >= 0 && size_t(m_layer_idx) < m_local_z_tool_change_idx.size())
m_local_z_tool_change_idx[size_t(m_layer_idx)] = 0;
if (m_layer_idx >= 0 && size_t(m_layer_idx) < m_local_z_reserve_slot_idx.size())
m_local_z_reserve_slot_idx[size_t(m_layer_idx)] = 0;
}
// If local_z_unplanned is true, emit a wipe/toolchange without consuming the preplanned
// per-layer wipe-tower sequence (used by Local-Z phase-b extra toolchanges).
std::string tool_change(GCode &gcodegen, int extruder_id, bool finish_layer, bool local_z_unplanned = false,
double local_z_nominal_layer_z = -1.);
bool is_empty_wipe_tower_gcode(GCode &gcodegen, int extruder_id, bool finish_layer);
std::string finalize(GCode &gcodegen);
std::vector<float> used_filament_length() const;
@@ -140,10 +156,15 @@ private:
// Reference to cached values at the Printer class.
const std::vector<WipeTower::ToolChangeResult> &m_priming;
const std::vector<std::vector<WipeTower::ToolChangeResult>> &m_tool_changes;
const std::vector<std::vector<WipeTower::ToolChangeResult>> &m_local_z_tool_changes;
const std::vector<std::vector<WipeTower::box_coordinates>> m_local_z_reserve_boxes;
const WipeTower::ToolChangeResult &m_final_purge;
// Current layer index.
int m_layer_idx;
int m_tool_change_idx;
std::vector<size_t> m_local_z_tool_change_idx;
// Per-layer next-available slot index for Local-Z unplanned toolchanges.
std::vector<size_t> m_local_z_reserve_slot_idx;
double m_last_wipe_tower_print_z;
// BBS
@@ -385,7 +406,26 @@ private:
void check_placeholder_parser_failed();
size_t get_extruder_id(unsigned int filament_id) const;
void set_last_pos(const Point &pos) { m_last_pos = Point3(pos, 0); m_last_pos_defined = true; }
// Mixed-filament resolution: convert a virtual 1-based filament ID to a zero-based
// physical extruder ID ready to pass to set_extruder(). When mixed_mgr is null or
// the ID is not a mixed slot the call is a no-op (returns virtual_id_1based - 1).
unsigned int resolve_extruder_for_layer(unsigned int virtual_id_1based,
const LayerTools &layer_tools) const
{
const unsigned int resolved_1based = layer_tools.resolve_mixed_1based(virtual_id_1based);
return resolved_1based == 0 ? 0 : resolved_1based - 1;
}
// Mixed-filament resolution: convert a virtual 1-based filament ID to a zero-based
// physical extruder ID ready to pass to set_extruder(). When mixed_mgr is null or
// the ID is not a mixed slot the call is a no-op (returns virtual_id_1based - 1).
unsigned int resolve_extruder_for_layer(unsigned int virtual_id_1based,
const LayerTools &layer_tools) const
{
const unsigned int resolved_1based = layer_tools.resolve_mixed_1based(virtual_id_1based);
return resolved_1based == 0 ? 0 : resolved_1based - 1;
}
void set_last_pos(const Point3 &pos) { m_last_pos = pos; m_last_pos_defined = true; }
bool last_pos_defined() const { return m_last_pos_defined; }
void set_extruders(const std::vector<unsigned int> &extruder_ids);
+249 -26
View File
@@ -20,6 +20,7 @@
#include <cassert>
#include <limits>
#include <algorithm>
#include <cmath>
#include <unordered_map>
#include <libslic3r.h>
@@ -36,6 +37,113 @@ namespace Slic3r {
const static bool g_wipe_into_objects = false;
constexpr double similar_color_threshold_de2000 = 20.0;
// ---------------------------------------------------------------------------
// Anonymous-namespace helpers for mixed-filament resolution
// ---------------------------------------------------------------------------
namespace {
// Resolve a 1-based filament ID through the mixed-filament manager, taking the
// optional per-object layer-height cadence (a/b) into account.
// Returns the resolved 1-based physical ID, or the input unchanged when the ID
// is not a mixed slot.
unsigned int resolve_mixed_with_layer_heights(const MixedFilamentManager *mixed_mgr,
size_t num_physical,
unsigned int filament_id_1based,
int layer_index,
float layer_print_z,
float layer_height,
float layer_height_a,
float layer_height_b,
float base_layer_height)
{
if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical)))
return filament_id_1based;
const MixedFilament *mixed_row = mixed_mgr->mixed_filament_from_id(filament_id_1based, num_physical);
const bool is_custom_mixed = mixed_row != nullptr && mixed_row->custom;
if (!is_custom_mixed && (layer_height_a > 0.f || layer_height_b > 0.f)) {
const float safe_base = std::max<float>(0.01f, base_layer_height);
const int ratio_a = std::max(1, int(std::lround((layer_height_a > 0.f ? layer_height_a : safe_base) / safe_base)));
const int ratio_b = std::max(1, int(std::lround((layer_height_b > 0.f ? layer_height_b : safe_base) / safe_base)));
const int cycle = ratio_a + ratio_b;
if (cycle > 0 && mixed_row != nullptr) {
const int pos = ((layer_index % cycle) + cycle) % cycle;
return pos < ratio_a ? mixed_row->component_a : mixed_row->component_b;
}
}
return mixed_mgr->resolve(filament_id_1based, num_physical, layer_index, layer_print_z, layer_height);
}
bool has_grouped_manual_pattern(const MixedFilamentManager *mixed_mgr,
size_t num_physical,
unsigned int filament_id_1based)
{
if (!(mixed_mgr && mixed_mgr->is_mixed(filament_id_1based, num_physical)))
return false;
const MixedFilament *mixed_row = mixed_mgr->mixed_filament_from_id(filament_id_1based, num_physical);
if (mixed_row == nullptr)
return false;
const std::string normalized = MixedFilamentManager::normalize_manual_pattern(mixed_row->manual_pattern);
return normalized.find(',') != std::string::npos;
}
bool internal_solid_infill_uses_sparse_filament(const PrintRegion &region, ExtrusionRole role)
{
return role == erSolidInfill && std::abs(region.config().sparse_infill_density.value - 100.) < EPSILON;
}
bool use_base_infill_filament_impl(const LayerTools &layer_tools, const PrintRegion &region)
{
const PrintRegionConfig &config = region.config();
if (!config.enable_infill_filament_override.value)
return true;
if (layer_tools.object_layer_count <= 0)
return false;
const int first_layers = std::max(0, config.infill_filament_use_base_first_layers.value);
const int last_layers = std::max(0, config.infill_filament_use_base_last_layers.value);
return layer_tools.layer_index < first_layers || layer_tools.layer_index >= layer_tools.object_layer_count - last_layers;
}
unsigned int sparse_infill_filament_id_1based_impl(const LayerTools &layer_tools, const PrintRegion &region)
{
return use_base_infill_filament_impl(layer_tools, region) ? region.config().wall_filament.value : region.config().sparse_infill_filament.value;
}
unsigned int grouped_manual_pattern_mixed_filament_id_for_layer(const LayerTools& layer_tools,
unsigned int configured_filament_id_1based)
{
if (layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0)
return 0;
if (has_grouped_manual_pattern(layer_tools.mixed_mgr, layer_tools.num_physical, configured_filament_id_1based))
return configured_filament_id_1based;
return 0;
}
unsigned int grouped_manual_pattern_infill_filament_1based(const LayerTools& layer_tools,
const PrintRegion& region,
unsigned int configured_filament_id_1based)
{
const unsigned int grouped_id =
grouped_manual_pattern_mixed_filament_id_for_layer(layer_tools, configured_filament_id_1based);
if (grouped_id == 0)
return 0;
const int innermost_perimeter_index = std::max(0, region.config().wall_loops.value - 1);
return layer_tools.mixed_mgr->resolve_perimeter(grouped_id,
layer_tools.num_physical,
layer_tools.layer_index,
innermost_perimeter_index,
float(layer_tools.print_z),
float(layer_tools.layer_height));
}
} // anonymous namespace
// ---------------------------------------------------------------------------
static std::set<int>get_filament_by_type(const std::vector<unsigned int>& used_filaments, const PrintConfig* print_config, const std::string& type)
{
std::set<int> target_filaments;
@@ -79,23 +187,50 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
return true;
}
// Resolve a 1-based filament ID through the mixed-filament manager for this layer.
unsigned int LayerTools::resolve_mixed_1based(unsigned int filament_id_1based) const
{
if (!mixed_mgr || filament_id_1based == 0)
return filament_id_1based;
return resolve_mixed_with_layer_heights(mixed_mgr,
num_physical,
filament_id_1based,
this->layer_index,
static_cast<float>(this->print_z),
static_cast<float>(this->layer_height),
mixed_layer_height_a,
mixed_layer_height_b,
mixed_base_layer_height);
}
// Return a zero based extruder from the region, or extruder_override if overriden.
unsigned int LayerTools::wall_filament(const PrintRegion &region) const
{
assert(region.config().wall_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().wall_filament.value : this->extruder_override) - 1;
unsigned int id_1based = (this->extruder_override == 0)
? region.config().wall_filament.value
: this->extruder_override;
return resolve_mixed_1based(id_1based) - 1;
}
unsigned int LayerTools::sparse_infill_filament(const PrintRegion &region) const
{
assert(region.config().sparse_infill_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().sparse_infill_filament.value : this->extruder_override) - 1;
unsigned int id_1based = (this->extruder_override == 0)
? sparse_infill_filament_id_1based_impl(*this, region)
: this->extruder_override;
const unsigned int grouped = grouped_manual_pattern_infill_filament_1based(*this, region, id_1based);
return ((grouped != 0) ? grouped : resolve_mixed_1based(id_1based)) - 1;
}
unsigned int LayerTools::solid_infill_filament(const PrintRegion &region) const
{
assert(region.config().solid_infill_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().solid_infill_filament.value : this->extruder_override) - 1;
unsigned int id_1based = (this->extruder_override == 0)
? region.config().solid_infill_filament.value
: this->extruder_override;
const unsigned int grouped = grouped_manual_pattern_infill_filament_1based(*this, region, id_1based);
return ((grouped != 0) ? grouped : resolve_mixed_1based(id_1based)) - 1;
}
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
@@ -104,20 +239,29 @@ unsigned int LayerTools::extruder(const ExtrusionEntityCollection &extrusions, c
assert(region.config().wall_filament.value > 0);
assert(region.config().sparse_infill_filament.value > 0);
assert(region.config().solid_infill_filament.value > 0);
// 1 based extruder ID.
unsigned int extruder = 1;
if (this->extruder_override == 0) {
if (extrusions.has_infill()) {
if (extrusions.has_solid_infill())
extruder = region.config().solid_infill_filament;
else
extruder = region.config().sparse_infill_filament;
} else
extruder = region.config().wall_filament.value;
} else
extruder = this->extruder_override;
if (extrusions.has_infill()) {
const ExtrusionRole role = extrusions.entities.empty() ? erNone : extrusions.entities.front()->role();
if (internal_solid_infill_uses_sparse_filament(region, role))
return sparse_infill_filament(region);
return is_solid_infill(role) ? solid_infill_filament(region) : sparse_infill_filament(region);
}
return wall_filament(region);
}
return (extruder == 0) ? 0 : extruder - 1;
unsigned int LayerTools::sparse_infill_filament_id_1based(const PrintRegion &region) const
{
return sparse_infill_filament_id_1based_impl(*this, region);
}
unsigned int LayerTools::infill_filament_id_1based(const PrintRegion &region) const
{
// Default role: erInternalInfill routes through sparse path.
return sparse_infill_filament_id_1based_impl(*this, region);
}
bool LayerTools::use_base_infill_filament(const PrintRegion &region) const
{
return use_base_infill_filament_impl(*this, region);
}
static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height)
@@ -384,6 +528,10 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude
m_print_full_config = &object.print()->full_print_config();
m_print_object_ptr = &object;
m_print = const_cast<Print*>(object.print());
// Mixed filament support.
m_mixed_mgr = &object.print()->mixed_filament_manager();
m_num_physical = object.print()->config().filament_colour.values.size();
update_mixed_layer_height_settings();
if (object.layers().empty())
return;
@@ -429,6 +577,10 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
m_print_full_config = &print.full_print_config();
m_print = const_cast<Print *>(&print); // for update the context of print
m_print_config_ptr = &print.config();
// Mixed filament support.
m_mixed_mgr = &print.mixed_filament_manager();
m_num_physical = print.config().filament_colour.values.size();
update_mixed_layer_height_settings();
// Initialize the print layers for all objects and all layers.
coordf_t max_layer_height = 0.;
@@ -481,6 +633,33 @@ ToolOrdering::ToolOrdering(const Print &print, unsigned int first_extruder, bool
this->mark_skirt_layers(print.config(), max_layer_height);
}
void ToolOrdering::update_mixed_layer_height_settings()
{
const PrintConfig *cfg = m_print_config_ptr;
if (cfg == nullptr && m_print_object_ptr != nullptr)
cfg = &m_print_object_ptr->print()->config();
m_mixed_layer_height_a = 0.f;
m_mixed_layer_height_b = 0.f;
if (m_print_full_config != nullptr &&
m_print_full_config->has("mixed_color_layer_height_a") &&
m_print_full_config->has("mixed_color_layer_height_b")) {
m_mixed_layer_height_a = float(m_print_full_config->opt_float("mixed_color_layer_height_a"));
m_mixed_layer_height_b = float(m_print_full_config->opt_float("mixed_color_layer_height_b"));
} else if (cfg != nullptr) {
m_mixed_layer_height_a = cfg->mixed_color_layer_height_a.value;
m_mixed_layer_height_b = cfg->mixed_color_layer_height_b.value;
}
float base_height = 0.2f;
if (m_print_object_ptr != nullptr)
base_height = float(m_print_object_ptr->config().layer_height.value);
else if (m_print_full_config != nullptr && m_print_full_config->has("layer_height"))
base_height = float(m_print_full_config->opt_float("layer_height"));
m_mixed_base_layer_height = std::max<float>(0.01f, base_height);
}
static void apply_first_layer_order(const DynamicPrintConfig* config, std::vector<unsigned int>& tool_order) {
const ConfigOptionInts* first_layer_print_sequence_op = config->option<ConfigOptionInts>("first_layer_print_sequence");
if (first_layer_print_sequence_op) {
@@ -646,6 +825,15 @@ void ToolOrdering::initialize_layers(std::vector<coordf_t> &zs)
// Collect extruders reuqired to print layers.
void ToolOrdering::collect_extruders(const PrintObject &object, const std::vector<std::pair<double, unsigned int>> &per_layer_extruder_switches)
{
// Propagate mixed-filament context to all LayerTools entries.
for (LayerTools &lt : m_layer_tools) {
lt.mixed_mgr = m_mixed_mgr;
lt.num_physical = m_num_physical;
lt.mixed_layer_height_a = m_mixed_layer_height_a;
lt.mixed_layer_height_b = m_mixed_layer_height_b;
lt.mixed_base_layer_height = m_mixed_base_layer_height;
}
// Extruder overrides are ordered by print_z.
std::vector<std::pair<double, unsigned int>>::const_iterator it_per_layer_extruder_override;
it_per_layer_extruder_override = per_layer_extruder_switches.begin();
@@ -659,6 +847,10 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
// Collect the object extruders.
for (auto layer : object.layers()) {
LayerTools &layer_tools = this->tools_for_layer(layer->print_z);
// Store the sequential layer index and height for mixed-filament resolution.
layer_tools.layer_index = layerCount;
layer_tools.object_layer_count = static_cast<int>(object.layers().size());
layer_tools.layer_height = layer->height;
// 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)
@@ -682,9 +874,19 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
if (something_nonoverriddable){
layer_tools.extruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override);
if (layerCount == 0) {
firstLayerExtruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override);
if (extruder_override == 0) {
layer_tools.extruders.emplace_back(layer_tools.wall_filament(region) + 1);
if (layerCount == 0) {
firstLayerExtruders.emplace_back(layer_tools.wall_filament(region) + 1);
}
} else {
const unsigned int resolved = resolve_mixed(extruder_override,
layerCount,
float(layer->print_z),
float(layer->height));
layer_tools.extruders.emplace_back(resolved);
if (layerCount == 0)
firstLayerExtruders.emplace_back(resolved);
}
}
@@ -710,13 +912,17 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
}
if (something_nonoverriddable || !m_print_config_ptr) {
if (extruder_override == 0) {
if (has_solid_infill)
layer_tools.extruders.emplace_back(region.config().solid_infill_filament);
if (has_infill)
layer_tools.extruders.emplace_back(region.config().sparse_infill_filament);
} else if (has_solid_infill || has_infill)
layer_tools.extruders.emplace_back(extruder_override);
if (extruder_override == 0) {
if (has_solid_infill)
layer_tools.extruders.emplace_back(layer_tools.solid_infill_filament(region) + 1);
if (has_infill)
layer_tools.extruders.emplace_back(layer_tools.sparse_infill_filament(region) + 1);
} else if (has_solid_infill || has_infill) {
layer_tools.extruders.emplace_back(resolve_mixed(extruder_override,
layerCount,
float(layer->print_z),
float(layer->height)));
}
}
if (has_solid_infill || has_infill)
layer_tools.has_object = true;
@@ -1833,5 +2039,22 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject
return -1;
}
// Resolve a 1-based filament ID through the mixed-filament manager.
unsigned int ToolOrdering::resolve_mixed(unsigned int filament_id_1based,
int layer_index,
float layer_print_z,
float layer_height) const
{
return resolve_mixed_with_layer_heights(m_mixed_mgr,
m_num_physical,
filament_id_1based,
layer_index,
layer_print_z,
layer_height,
m_mixed_layer_height_a,
m_mixed_layer_height_b,
m_mixed_base_layer_height);
}
} // namespace Slic3r
+49
View File
@@ -4,6 +4,7 @@
#define slic3r_ToolOrdering_hpp_
#include "../libslic3r.h"
#include "../MixedFilament.hpp"
#include <utility>
@@ -145,6 +146,12 @@ public:
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion &region) const;
// Stub helpers for per-layer infill override (Task 14). Each returns
// the configured 1-based filament ID without per-layer logic until Task 14 lands.
unsigned int sparse_infill_filament_id_1based(const PrintRegion &region) const;
unsigned int infill_filament_id_1based(const PrintRegion &region) const;
bool use_base_infill_filament(const PrintRegion &region) const;
coordf_t print_z = 0.;
bool has_object = false;
bool has_support = false;
@@ -153,6 +160,27 @@ public:
// If per layer extruder switches are inserted by the G-code preview slider, this value contains the new (1 based) extruder, with which the whole object layer is being printed with.
// If not overriden, it is set to 0.
unsigned int extruder_override = 0;
// Mixed-filament resolution context (set by ToolOrdering during collect_extruders).
const MixedFilamentManager *mixed_mgr = nullptr;
size_t num_physical = 0;
// Sequential layer index (0-based), used by mixed-filament resolution.
int layer_index = -1;
// Total number of object layers for the current print object.
int object_layer_count = 0;
// Actual layer height for this print_z where available.
coordf_t layer_height = 0.;
// Optional mixed-layer cadence override from print settings.
float mixed_layer_height_a = 0.f;
float mixed_layer_height_b = 0.f;
float mixed_base_layer_height = 0.2f;
// Resolve a configured 1-based filament ID to a physical 1-based ID via the
// mixed-filament manager for this layer. Returns input unchanged if no manager
// is set or the ID is not a mixed slot.
bool is_mixed_slot_0based(unsigned int filament_id_0based) const {
return mixed_mgr && mixed_mgr->is_mixed(filament_id_0based + 1, num_physical);
}
// Should a skirt be printed at this layer?
// Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed.
bool has_skirt = false;
@@ -172,6 +200,10 @@ public:
return m_wiping_extrusions;
}
// Resolve a 1-based filament ID through the mixed-filament manager for this layer.
// Returns input unchanged when mixed_mgr is null or ID is not a mixed slot.
unsigned int resolve_mixed_1based(unsigned int filament_id_1based) const;
private:
// This object holds list of extrusion that will be used for extruder wiping
WipingExtrusions m_wiping_extrusions;
@@ -254,6 +286,14 @@ public:
bool has_non_support_filament(const PrintConfig &config);
// Resolve a 1-based filament ID through the mixed-filament manager.
// Returns the resolved physical extruder (1-based). If the ID is not a
// mixed filament or no manager is set, returns the input unchanged.
unsigned int resolve_mixed(unsigned int filament_id_1based,
int layer_index,
float layer_print_z = 0.f,
float layer_height = 0.f) const;
private:
void initialize_layers(std::vector<coordf_t> &zs);
void collect_extruders(const PrintObject &object, const std::vector<std::pair<double, unsigned int>> &per_layer_extruder_switches);
@@ -262,6 +302,8 @@ private:
void mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height);
void collect_extruder_statistics(bool prime_multi_material);
void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer);
// Read mixed_color_layer_height_a/b from config and cache in m_mixed_layer_height_*.
void update_mixed_layer_height_settings();
// BBS
std::vector<unsigned int> generate_first_layer_tool_order(const Print& print);
@@ -278,6 +320,13 @@ private:
const PrintConfig* m_print_config_ptr = nullptr;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
// Mixed filament support: pointer to manager (owned by Print) and
// number of physical extruders.
const MixedFilamentManager* m_mixed_mgr = nullptr;
size_t m_num_physical = 0;
float m_mixed_layer_height_a = 0.f;
float m_mixed_layer_height_b = 0.f;
float m_mixed_base_layer_height = 0.2f;
bool m_sorted = false;
FilamentChangeStats m_stats_by_single_extruder;
+369 -76
View File
@@ -6,6 +6,7 @@
#include <vector>
#include <numeric>
#include <memory>
#include <limits>
#include <sstream>
#include <iomanip>
@@ -19,6 +20,7 @@
#include "Fill/FillRectilinear.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r
@@ -30,10 +32,16 @@ static const double wipe_tower_wall_infill_overlap = 0.0;
static constexpr double WIPE_TOWER_RESOLUTION = 0.1;
static constexpr double WT_SIMPLIFY_TOLERANCE_SCALED = 0.001f / SCALING_FACTOR_INTERNAL;
static constexpr int arc_fit_size = 20;
static constexpr size_t LEGACY_NO_TOOL = static_cast<size_t>(std::numeric_limits<unsigned int>::max());
#define SCALED_WIPE_TOWER_RESOLUTION (WIPE_TOWER_RESOLUTION / SCALING_FACTOR_INTERNAL)
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow };
static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}};
inline bool is_no_tool_sentinel(size_t tool)
{
return tool == LEGACY_NO_TOOL || tool == size_t(-1);
}
inline float align_round(float value, float base) { return std::round(value / base) * base; }
inline float align_ceil(float value, float base) { return std::ceil(value / base) * base; }
@@ -1257,6 +1265,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_extra_flow(float(config.wipe_tower_extra_flow/100.)),
m_extra_spacing_wipe(float(config.wipe_tower_extra_spacing/100. * config.wipe_tower_extra_flow/100.)),
m_extra_spacing_ramming(float(config.wipe_tower_extra_spacing/100.)),
m_local_z_wipe_tower_purge_lines(float(config.local_z_wipe_tower_purge_lines)),
m_y_shift(0.f),
m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)),
@@ -1515,49 +1524,50 @@ std::vector<WipeTower::ToolChangeResult> WipeTower2::prime(
return results;
}
WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
WipeTower::ToolChangeResult WipeTower2::emit_planned_tool_change(const WipeTowerInfo::ToolChange *tool_change)
{
size_t old_tool = m_current_tool;
if (tool_change != nullptr && m_current_tool != tool_change->old_tool) {
BOOST_LOG_TRIVIAL(warning) << "Wipe tower tool state mismatch, realigning to planned toolchange"
<< " layer_z=" << (m_layer_info != m_plan.end() ? m_layer_info->z : -1.f)
<< " current_tool=" << m_current_tool
<< " planned_old_tool=" << tool_change->old_tool
<< " planned_new_tool=" << tool_change->new_tool;
m_current_tool = tool_change->old_tool;
}
float wipe_area = 0.f;
float wipe_volume = 0.f;
bool interface_layer = m_enable_tower_interface_features && m_current_layer_has_interface;
// Finds this toolchange info
if (tool != (unsigned int)(-1))
{
for (const auto &b : m_layer_info->tool_changes)
if ( b.new_tool == tool ) {
wipe_volume = b.wipe_volume;
wipe_area = b.required_depth;
break;
}
}
else {
// Otherwise we are going to Unload only. And m_layer_info would be invalid.
}
if (interface_layer && tool != (unsigned int)(-1) && tool < m_filpar.size()) {
const size_t tool = tool_change != nullptr ? tool_change->new_tool : LEGACY_NO_TOOL;
const size_t old_tool = m_current_tool;
float wipe_area = 0.f;
float wipe_volume = 0.f;
bool interface_layer = m_enable_tower_interface_features && m_current_layer_has_interface;
if (tool_change != nullptr) {
wipe_volume = tool_change->wipe_volume;
wipe_area = tool_change->required_depth;
}
if (interface_layer && !is_no_tool_sentinel(tool) && tool < m_filpar.size()) {
float extra_purge_length = m_filpar[tool].tower_interface_purge_length;
if (extra_purge_length > 0.f) {
wipe_volume += extra_purge_length * m_filpar[tool].filament_area;
}
}
WipeTower::box_coordinates cleaning_box(
Vec2f(m_perimeter_width / 2.f, m_perimeter_width / 2.f),
m_wipe_tower_width - m_perimeter_width,
(tool != (unsigned int)(-1) ? wipe_area+m_depth_traversed-0.5f*m_perimeter_width
: m_wipe_tower_depth-m_perimeter_width));
WipeTower::box_coordinates cleaning_box(Vec2f(m_perimeter_width / 2.f, m_perimeter_width / 2.f), m_wipe_tower_width - m_perimeter_width,
(!is_no_tool_sentinel(tool) ? wipe_area + m_depth_traversed - 0.5f * m_perimeter_width :
m_wipe_tower_depth - m_perimeter_width));
WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting);
writer.set_extrusion_flow(m_extrusion_flow)
.set_z(m_z_pos)
.set_initial_tool(m_current_tool)
.set_y_shift(m_y_shift + (tool!=(unsigned int)(-1) && (m_current_shape == SHAPE_REVERSED) ? m_layer_info->depth - m_layer_info->toolchanges_depth(): 0.f))
.append(";--------------------\n"
"; CP TOOLCHANGE START\n");
WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting);
writer.set_extrusion_flow(m_extrusion_flow)
.set_z(m_z_pos)
.set_initial_tool(m_current_tool)
.set_y_shift(m_y_shift + (!is_no_tool_sentinel(tool) && (m_current_shape == SHAPE_REVERSED) ?
m_layer_info->depth - m_layer_info->toolchanges_depth() :
0.f))
.append(";--------------------\n"
"; CP TOOLCHANGE START\n");
if (tool != (unsigned)(-1)){
if (!is_no_tool_sentinel(tool)) {
writer.comment_with_value(" toolchange #", m_num_tool_changes + 1); // the number is zero-based
writer.append(std::string("; material : " + (m_current_tool < m_filpar.size() ? m_filpar[m_current_tool].material : "(NONE)") + " -> " + m_filpar[tool].material + "\n").c_str())
.append(";--------------------\n");
@@ -1574,8 +1584,10 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
if (m_set_extruder_trimpot)
writer.set_extruder_trimpot(750);
m_active_tool_change = tool_change;
// Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool.
if (tool != (unsigned int)-1){ // This is not the last change.
if (!is_no_tool_sentinel(tool)) { // This is not the last change.
auto new_tool_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature;
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material,
(is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature),
@@ -1609,6 +1621,7 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
} else
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, m_filpar[m_current_tool].temperature, m_filpar[m_current_tool].temperature);
m_active_tool_change = nullptr;
m_depth_traversed += wipe_area;
if (m_set_extruder_trimpot)
@@ -1625,7 +1638,46 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
return construct_tcr(writer, false, old_tool, false, interface_layer);
WipeTower::ToolChangeResult result = construct_tcr(writer, false, old_tool, false, interface_layer);
result.purge_volume = wipe_volume;
return result;
}
WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
{
const WipeTowerInfo::ToolChange *planned_tool_change = nullptr;
if (!is_no_tool_sentinel(tool)) {
for (const WipeTowerInfo::ToolChange &entry : m_layer_info->tool_changes) {
if (entry.old_tool == m_current_tool && entry.new_tool == tool) {
planned_tool_change = &entry;
break;
}
}
if (planned_tool_change == nullptr) {
for (const WipeTowerInfo::ToolChange &entry : m_layer_info->tool_changes) {
if (entry.new_tool == tool) {
planned_tool_change = &entry;
break;
}
}
}
if (planned_tool_change == nullptr) {
std::ostringstream planned_sequence;
for (size_t idx = 0; idx < m_layer_info->tool_changes.size(); ++idx) {
if (idx != 0)
planned_sequence << ",";
planned_sequence << m_layer_info->tool_changes[idx].old_tool << "->" << m_layer_info->tool_changes[idx].new_tool;
}
BOOST_LOG_TRIVIAL(error) << "Wipe tower toolchange not found in plan"
<< " layer_z=" << (m_layer_info != m_plan.end() ? m_layer_info->z : -1.f)
<< " current_tool=" << m_current_tool
<< " requested_new_tool=" << tool
<< " planned_sequence=[" << planned_sequence.str() << "]";
throw Slic3r::RuntimeError("Wipe tower toolchange not found in plan.");
}
}
return emit_planned_tool_change(planned_tool_change);
}
@@ -1678,22 +1730,15 @@ void WipeTower2::toolchange_Unload(
else
sparse_beginning_y += (m_layer_info-1)->toolchanges_depth() + m_perimeter_width;
float sum_of_depths = 0.f;
for (const auto& tch : m_layer_info->tool_changes) { // let's find this toolchange
if (tch.old_tool == m_current_tool) {
sum_of_depths += tch.ramming_depth;
float ramming_end_y = sum_of_depths;
ramming_end_y -= (y_step/m_extra_spacing_ramming-m_perimeter_width) / 2.f; // center of final ramming line
if (m_active_tool_change != nullptr) {
float ramming_end_y = cumulative_toolchange_depth_before(m_active_tool_change) + m_active_tool_change->ramming_depth;
ramming_end_y -= (y_step / m_extra_spacing_ramming - m_perimeter_width) / 2.f; // center of final ramming line
if ( (m_current_shape == SHAPE_REVERSED && ramming_end_y < sparse_beginning_y - 0.5f*m_perimeter_width ) ||
(m_current_shape == SHAPE_NORMAL && ramming_end_y > sparse_beginning_y + 0.5f*m_perimeter_width ) )
{
writer.extrude(xl + tch.first_wipe_line-1.f*m_perimeter_width,writer.y());
remaining -= tch.first_wipe_line-1.f*m_perimeter_width;
}
break;
if ((m_current_shape == SHAPE_REVERSED && ramming_end_y < sparse_beginning_y - 0.5f * m_perimeter_width) ||
(m_current_shape == SHAPE_NORMAL && ramming_end_y > sparse_beginning_y + 0.5f * m_perimeter_width)) {
writer.extrude(xl + m_active_tool_change->first_wipe_line - 1.f * m_perimeter_width, writer.y());
remaining -= m_active_tool_change->first_wipe_line - 1.f * m_perimeter_width;
}
sum_of_depths += tch.required_depth;
}
}
@@ -1989,8 +2034,16 @@ void WipeTower2::toolchange_Wipe(
.add_wipe_point(writer.x(), writer.y() - dy)
.add_wipe_point(! m_left_to_right ? m_wipe_tower_width : 0.f, writer.y() - dy);
if (m_layer_info != m_plan.end() && m_current_tool != m_layer_info->tool_changes.back().new_tool)
m_left_to_right = !m_left_to_right;
if (m_layer_info != m_plan.end()) {
size_t final_tool_on_layer = m_current_tool;
if (!m_layer_info->tool_changes.empty())
final_tool_on_layer = m_layer_info->tool_changes.back().new_tool;
else if (!m_layer_info->local_z_tool_changes.empty())
final_tool_on_layer = m_layer_info->local_z_tool_changes.back().new_tool;
if (m_current_tool != final_tool_on_layer)
m_left_to_right = !m_left_to_right;
}
writer.set_extrusion_flow(m_extrusion_flow); // Reset the extrusion flow.
writer.change_analyzer_line_width(m_perimeter_width);
@@ -2019,7 +2072,8 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
if (m_enable_tower_interface_features && m_prev_layer_had_interface)
feedrate = std::min(feedrate, 20.f * 60.f);
float current_depth = m_layer_info->depth - m_layer_info->toolchanges_depth();
float reserve_depth = m_layer_info->local_z_reserve_depth();
float current_depth = std::max(0.f, m_layer_info->depth - m_layer_info->toolchanges_depth() - reserve_depth);
WipeTower::box_coordinates fill_box(Vec2f(m_perimeter_width, m_layer_info->depth-(current_depth-m_perimeter_width)),
m_wipe_tower_width - 2 * m_perimeter_width, current_depth-m_perimeter_width);
@@ -2050,14 +2104,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Is there a soluble filament wiped/rammed at the next layer?
// If so, the infill should not be sparse.
bool solid_infill = m_layer_info+1 == m_plan.end()
? false
: std::any_of((m_layer_info+1)->tool_changes.begin(),
(m_layer_info+1)->tool_changes.end(),
[this](const WipeTowerInfo::ToolChange& tch) {
return m_filpar[tch.new_tool].is_soluble
|| m_filpar[tch.old_tool].is_soluble;
});
bool solid_infill = m_layer_info + 1 == m_plan.end() ? false : layer_has_soluble_toolchange(*(m_layer_info + 1));
solid_infill |= first_layer && m_adhesion;
if (solid_infill) {
@@ -2249,6 +2296,205 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
m_plan.back().tool_changes.push_back(WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume));
}
void WipeTower2::plan_local_z_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume)
{
assert(m_plan.empty() || m_plan.back().z <= z_par + WT_EPSILON);
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par)
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (!m_no_sparse_layers || old_tool != new_tool || m_plan.size() == 1))
m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool)
return;
float width = m_wipe_tower_width - 3 * m_perimeter_width;
float length_to_extrude = volume_to_length(0.25f * std::accumulate(m_filpar[old_tool].ramming_speed.begin(),
m_filpar[old_tool].ramming_speed.end(), 0.f),
m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator, layer_height_par);
float ramming_depth = m_enable_filament_ramming ? ((int(length_to_extrude / width) + 1) *
(m_perimeter_width * m_filpar[old_tool].ramming_line_width_multiplicator *
m_filpar[old_tool].ramming_step_multiplicator) *
m_extra_spacing_ramming) :
0;
float first_wipe_line = -(width * ((length_to_extrude / width) - int(length_to_extrude / width)) - width);
float first_wipe_volume = length_to_volume(first_wipe_line, m_perimeter_width * m_extra_flow, layer_height_par);
float wiping_depth = get_wipe_depth(wipe_volume - first_wipe_volume, layer_height_par, m_perimeter_width, m_extra_flow,
m_extra_spacing_wipe, width);
m_plan.back().local_z_tool_changes.push_back(
WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume));
}
void WipeTower2::plan_local_z_reserve(float z_par, float layer_height_par, size_t reserve_slot_count, float wipe_volume)
{
if (reserve_slot_count == 0)
return;
assert(m_plan.empty() || m_plan.back().z <= z_par + WT_EPSILON);
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par)
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
const float mini_wipe_depth = m_local_z_wipe_tower_purge_lines * m_perimeter_width * m_extra_spacing_wipe;
const float wipe_width = std::max(0.f, m_wipe_tower_width - 3.f * m_perimeter_width);
const float wiping_depth = wipe_width > WT_EPSILON ?
get_wipe_depth(std::max(0.f, wipe_volume), layer_height_par, m_perimeter_width, m_extra_flow,
m_extra_spacing_wipe, wipe_width) :
0.f;
float max_ramming_depth = 0.f;
if (wipe_width > WT_EPSILON) {
for (const FilamentParameters& filament : m_filpar) {
const bool do_ramming = (m_semm && m_enable_filament_ramming) || filament.multitool_ramming;
if (!do_ramming || filament.ramming_speed.empty())
continue;
const float line_width = m_perimeter_width * filament.ramming_line_width_multiplicator;
const float line_step =
(m_perimeter_width * filament.ramming_line_width_multiplicator * filament.ramming_step_multiplicator) *
m_extra_spacing_ramming;
if (line_width <= WT_EPSILON || line_step <= WT_EPSILON)
continue;
const float ramming_volume = 0.25f * std::accumulate(filament.ramming_speed.begin(), filament.ramming_speed.end(), 0.f);
const float length_to_extrude = volume_to_length(ramming_volume, line_width, layer_height_par);
const float ramming_depth =
(float(int(length_to_extrude / wipe_width) + 1) * line_step);
max_ramming_depth = std::max(max_ramming_depth, ramming_depth);
}
}
const float full_toolchange_depth = max_ramming_depth + wiping_depth;
const float slot_depth =
std::max(2.5f * m_perimeter_width, std::max(mini_wipe_depth + m_perimeter_width, full_toolchange_depth + m_perimeter_width));
WipeTowerInfo &layer = m_plan.back();
layer.local_z_reserve_slot_depth = std::max(layer.local_z_reserve_slot_depth, slot_depth);
layer.local_z_reserve_slot_count += reserve_slot_count;
}
WipeTower::ToolChangeResult WipeTower2::local_z_tool_change(size_t new_tool,
const WipeTower::box_coordinates& cleaning_box,
float wipe_volume)
{
const size_t old_tool = m_current_tool;
WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting);
writer.set_extrusion_flow(m_extrusion_flow)
.set_z(m_z_pos)
.set_initial_tool(m_current_tool)
.append(";--------------------\n"
"; CP TOOLCHANGE START\n");
writer.comment_with_value(" toolchange #", m_num_tool_changes + 1);
writer.append(std::string("; material : " + (m_current_tool < m_filpar.size() ? m_filpar[m_current_tool].material : "(NONE)") + " -> " +
m_filpar[new_tool].material + "\n")
.c_str())
.append(";--------------------\n");
writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n");
writer.speed_override_backup();
writer.speed_override(100);
writer.set_initial_position(cleaning_box.ld, m_wipe_tower_width, m_wipe_tower_depth, 0.f);
if (m_set_extruder_trimpot)
writer.set_extruder_trimpot(750);
const int old_tool_temp = is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature;
const int new_tool_temp = is_first_layer() ? m_filpar[new_tool].first_layer_temperature : m_filpar[new_tool].temperature;
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, old_tool_temp, new_tool_temp);
toolchange_Change(writer, new_tool, m_filpar[new_tool].material);
toolchange_Load(writer, cleaning_box);
writer.travel(writer.x(), writer.y() - m_perimeter_width);
toolchange_Wipe(writer, cleaning_box, wipe_volume, false);
writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_End) + "\n");
++m_num_tool_changes;
if (m_set_extruder_trimpot)
writer.set_extruder_trimpot(550);
writer.speed_override_restore();
writer.feedrate(m_travel_speed * 60.f)
.flush_planner_queue()
.reset_extruder()
.append("; CP TOOLCHANGE END\n"
";------------------\n"
"\n\n");
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
WipeTower::ToolChangeResult result = construct_tcr(writer, false, old_tool, false);
result.purge_volume = wipe_volume;
return result;
}
namespace {
// Helper: rotate a point within the wipe tower local coordinate system
// by internal_angle_deg, shifting by y_shift.
static Vec2f rotate_local_z_reserve_point(const Vec2f& pt, float tower_width, float tower_depth,
float y_shift, float internal_angle_deg)
{
Vec2f shifted = pt;
shifted.x() -= tower_width / 2.f;
shifted.y() += y_shift - tower_depth / 2.f;
const double angle = internal_angle_deg * double(M_PI / 180.);
const double c = std::cos(angle);
const double s = std::sin(angle);
return Vec2f(float(shifted.x() * c - shifted.y() * s) + tower_width / 2.f,
float(shifted.x() * s + shifted.y() * c) + tower_depth / 2.f);
}
} // namespace
std::vector<std::vector<WipeTower::box_coordinates>> WipeTower2::get_local_z_reserve_boxes() const
{
std::vector<std::vector<WipeTower::box_coordinates>> out;
out.reserve(m_plan.size());
for (size_t layer_idx = 0; layer_idx < m_plan.size(); ++layer_idx) {
const WipeTowerInfo& layer = m_plan[layer_idx];
std::vector<WipeTower::box_coordinates> layer_boxes;
layer_boxes.reserve(layer.local_z_reserve_slot_count);
if (layer.local_z_reserve_slot_count > 0 && layer.local_z_reserve_slot_depth > WT_EPSILON) {
const float y_shift = layer.depth < m_wipe_tower_depth - m_perimeter_width ?
(m_wipe_tower_depth - layer.depth - m_perimeter_width) / 2.f : 0.f;
const float internal_angle = (layer_idx % 2 == 0) ? 180.f : 0.f;
for (size_t slot_idx = 0; slot_idx < layer.local_z_reserve_slot_count; ++slot_idx) {
const float slot_start = layer.toolchanges_depth() + float(slot_idx) * layer.local_z_reserve_slot_depth;
const float width = std::max(0.f, m_wipe_tower_width - 2.f * m_perimeter_width);
const float height = std::max(0.f, layer.local_z_reserve_slot_depth - m_perimeter_width);
if (width <= WT_EPSILON || height <= WT_EPSILON)
continue;
const Vec2f ld_unrotated(m_perimeter_width, slot_start + m_perimeter_width);
const Vec2f rd_unrotated = ld_unrotated + Vec2f(width, 0.f);
const Vec2f ru_unrotated = ld_unrotated + Vec2f(width, height);
const Vec2f lu_unrotated = ld_unrotated + Vec2f(0.f, height);
const Vec2f ld = rotate_local_z_reserve_point(ld_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle);
const Vec2f rd = rotate_local_z_reserve_point(rd_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle);
const Vec2f ru = rotate_local_z_reserve_point(ru_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle);
const Vec2f lu = rotate_local_z_reserve_point(lu_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle);
const float min_x = std::min({ld.x(), rd.x(), ru.x(), lu.x()});
const float max_x = std::max({ld.x(), rd.x(), ru.x(), lu.x()});
const float min_y = std::min({ld.y(), rd.y(), ru.y(), lu.y()});
const float max_y = std::max({ld.y(), rd.y(), ru.y(), lu.y()});
layer_boxes.emplace_back(Vec2f(min_x, min_y), max_x - min_x, max_y - min_y);
}
}
out.emplace_back(std::move(layer_boxes));
}
return out;
}
void WipeTower2::plan_tower()
@@ -2262,7 +2508,7 @@ void WipeTower2::plan_tower()
for (int layer_index = int(m_plan.size()) - 1; layer_index >= 0; --layer_index)
{
float this_layer_depth = std::max(m_plan[layer_index].depth, m_plan[layer_index].toolchanges_depth());
float this_layer_depth = std::max(m_plan[layer_index].depth, m_plan[layer_index].planned_depth());
m_plan[layer_index].depth = this_layer_depth;
if (this_layer_depth > m_wipe_tower_depth - m_perimeter_width)
@@ -2293,7 +2539,7 @@ void WipeTower2::save_on_last_wipe()
for (int i=0; i<int(m_layer_info->tool_changes.size()); ++i) {
auto& toolchange = m_layer_info->tool_changes[i];
tool_change(toolchange.new_tool);
emit_planned_tool_change(&toolchange);
if (i == idx) {
float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into
@@ -2339,8 +2585,38 @@ int WipeTower2::first_toolchange_to_nonsoluble(
return tool_changes.empty() ? -1 : 0;
}
static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first,
WipeTower::ToolChangeResult& second)
bool WipeTower2::layer_has_soluble_toolchange(const WipeTowerInfo &layer) const
{
auto has_soluble = [this](const std::vector<WipeTowerInfo::ToolChange> &tool_changes) {
return std::any_of(tool_changes.begin(), tool_changes.end(), [this](const WipeTowerInfo::ToolChange &toolchange) {
return m_filpar[toolchange.new_tool].is_soluble || m_filpar[toolchange.old_tool].is_soluble;
});
};
return has_soluble(layer.local_z_tool_changes) || has_soluble(layer.tool_changes);
}
float WipeTower2::cumulative_toolchange_depth_before(const WipeTowerInfo::ToolChange *tool_change) const
{
if (tool_change == nullptr || m_layer_info == m_plan.end())
return 0.f;
float depth = 0.f;
for (const WipeTowerInfo::ToolChange &entry : m_layer_info->local_z_tool_changes) {
if (&entry == tool_change)
return depth;
depth += entry.required_depth;
}
for (const WipeTowerInfo::ToolChange &entry : m_layer_info->tool_changes) {
if (&entry == tool_change)
return depth;
depth += entry.required_depth;
}
return depth;
}
static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, WipeTower::ToolChangeResult& second)
{
assert(first.new_tool == second.initial_tool);
WipeTower::ToolChangeResult out = first;
@@ -2358,10 +2634,11 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first,
return out;
}
// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower
// Resulting ToolChangeResults are appended into vector "result"
void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>> &result)
// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower.
// Normal per-layer toolchanges are appended into "result", while Local-Z phase-b toolchanges are
// emitted into "local_z_result" so G-code can consume them before the nominal layer loop.
void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>& result,
std::vector<std::vector<WipeTower::ToolChangeResult>>& local_z_result)
{
if (m_plan.empty())
return;
@@ -2383,8 +2660,12 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
m_layer_info = m_plan.begin();
m_current_height = 0.f;
// we don't know which extruder to start with - we'll set it according to the first toolchange
// We don't know which extruder to start with, so take the first actual toolchange on the tower.
for (const auto& layer : m_plan) {
if (!layer.local_z_tool_changes.empty()) {
m_current_tool = layer.local_z_tool_changes.front().old_tool;
break;
}
if (!layer.tool_changes.empty()) {
m_current_tool = layer.tool_changes.front().old_tool;
break;
@@ -2400,7 +2681,15 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
for (const WipeTower2::WipeTowerInfo& layer : m_plan)
{
std::vector<WipeTower::ToolChangeResult> layer_result;
set_layer(layer.z, layer.height, 0, false/*layer.z == m_plan.front().z*/, layer.z == m_plan.back().z);
std::vector<WipeTower::ToolChangeResult> local_z_layer_result;
BOOST_LOG_TRIVIAL(debug) << "Wipe tower layer plan"
<< " z=" << layer.z
<< " height=" << layer.height
<< " nominal_toolchanges=" << layer.tool_changes.size()
<< " local_z_toolchanges=" << layer.local_z_tool_changes.size()
<< " reserve_slots=" << layer.local_z_reserve_slot_count
<< " planned_depth=" << layer.planned_depth();
set_layer(layer.z, layer.height, 0, false /*layer.z == m_plan.front().z*/, layer.z == m_plan.back().z);
m_internal_rotation += 180.f;
if (m_layer_info->depth < m_wipe_tower_depth - m_perimeter_width)
@@ -2409,15 +2698,18 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
int idx = first_toolchange_to_nonsoluble(layer.tool_changes);
WipeTower::ToolChangeResult finish_layer_tcr;
for (const WipeTowerInfo::ToolChange &toolchange : layer.local_z_tool_changes)
local_z_layer_result.emplace_back(emit_planned_tool_change(&toolchange));
if (idx == -1) {
// if there is no toolchange switching to non-soluble, finish layer
// will be called at the very beginning. That's the last possibility
// where a nonsoluble tool can be.
// If there is no nominal-layer toolchange switching to non-soluble,
// finish_layer still runs after any Local-Z toolchanges already planned
// onto this tower layer.
finish_layer_tcr = finish_layer();
}
for (int i=0; i<int(layer.tool_changes.size()); ++i) {
layer_result.emplace_back(tool_change(layer.tool_changes[i].new_tool));
for (int i = 0; i < int(layer.tool_changes.size()); ++i) {
layer_result.emplace_back(emit_planned_tool_change(&layer.tool_changes[i]));
if (i == idx) // finish_layer will be called after this toolchange
finish_layer_tcr = finish_layer();
}
@@ -2435,7 +2727,8 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
layer_result[idx] = merge_tcr(layer_result[idx], finish_layer_tcr);
}
result.emplace_back(std::move(layer_result));
result.emplace_back(std::move(layer_result));
local_z_result.emplace_back(std::move(local_z_layer_result));
if (m_used_filament_length_until_layer.empty() || m_used_filament_length_until_layer.back().first != layer.z)
m_used_filament_length_until_layer.emplace_back();
+26 -2
View File
@@ -51,12 +51,17 @@ public:
// Appends into internal structure m_plan containing info about the future wipe tower
// to be used before building begins. The entries must be added ordered in z.
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f);
void plan_local_z_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f);
// Reserve Local-Z wipe-tower slots for unplanned toolchanges during Local-Z sub-layer emission.
void plan_local_z_reserve(float z_par, float layer_height_par, size_t reserve_slot_count, float wipe_volume = 0.f);
// Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result"
void generate(std::vector<std::vector<WipeTower::ToolChangeResult>> &result);
void generate(std::vector<std::vector<WipeTower::ToolChangeResult>> &result,
std::vector<std::vector<WipeTower::ToolChangeResult>> &local_z_result);
float get_depth() const { return m_wipe_tower_depth; }
std::vector<std::pair<float, float>> get_z_and_depth_pairs() const;
std::vector<std::vector<WipeTower::box_coordinates>> get_local_z_reserve_boxes() const;
float get_brim_width() const { return m_wipe_tower_brim_width_real; }
float get_wipe_tower_height() const { return m_wipe_tower_height; }
// ORCA: Match WipeTower API used by Print skirt/brim planning.
@@ -132,6 +137,9 @@ public:
// Returns gcode for a toolchange and a final print head position.
// On the first layer, extrude a brim around the future wipe tower first.
WipeTower::ToolChangeResult tool_change(size_t new_tool);
// Emit a mini toolchange into a pre-reserved Local-Z wipe slot (does not consume m_plan).
WipeTower::ToolChangeResult local_z_tool_change(size_t new_tool, const WipeTower::box_coordinates& cleaning_box, float wipe_volume);
void set_current_tool(size_t tool) { m_current_tool = tool; }
// Fill the unfilled space with a sparse infill.
// Call this method only if layer_finished() is false.
@@ -182,6 +190,8 @@ public:
};
private:
struct WipeTowerInfo;
enum wipe_shape // A fill-in direction
{
SHAPE_NORMAL = 1,
@@ -260,6 +270,9 @@ private:
// Extruder specific parameters.
std::vector<FilamentParameters> m_filpar;
// Number of wipe-tower purge lines to reserve per Local-Z unplanned toolchange slot.
float m_local_z_wipe_tower_purge_lines = 3.f;
// State of the wipe tower generator.
unsigned int m_num_layer_changes = 0; // Layer change counter for the output statistics.
unsigned int m_num_tool_changes = 0; // Tool change change counter for the output statistics.
@@ -309,9 +322,16 @@ private:
float z; // z position of the layer
float height; // layer height
float depth; // depth of the layer based on all layers above
float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; }
float normal_toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; }
float local_z_toolchanges_depth() const { float sum = 0.f; for (const auto &a : local_z_tool_changes) sum += a.required_depth; return sum; }
float toolchanges_depth() const { return normal_toolchanges_depth() + local_z_toolchanges_depth(); }
float local_z_reserve_slot_depth { 0.f };
size_t local_z_reserve_slot_count { 0 };
float local_z_reserve_depth() const { return local_z_reserve_slot_depth * float(local_z_reserve_slot_count); }
float planned_depth() const { return toolchanges_depth() + local_z_reserve_depth(); }
std::vector<ToolChange> tool_changes;
std::vector<ToolChange> local_z_tool_changes;
WipeTowerInfo(float z_par, float layer_height_par)
: z{z_par}, height{layer_height_par}, depth{0} {}
@@ -319,6 +339,7 @@ private:
std::vector<WipeTowerInfo> m_plan; // Stores information about all layers and toolchanges for the future wipe tower (filled by plan_toolchange(...))
std::vector<WipeTowerInfo>::iterator m_layer_info = m_plan.end();
const WipeTowerInfo::ToolChange *m_active_tool_change = nullptr;
// This sums height of all extruded layers, not counting the layers which
// will be later removed when the "no_sparse_layers" is used.
@@ -332,6 +353,9 @@ private:
// ot -1 if there is no such toolchange.
int first_toolchange_to_nonsoluble(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
bool layer_has_soluble_toolchange(const WipeTowerInfo &layer) const;
float cumulative_toolchange_depth_before(const WipeTowerInfo::ToolChange *tool_change) const;
WipeTower::ToolChangeResult emit_planned_tool_change(const WipeTowerInfo::ToolChange *tool_change);
void toolchange_Unload(
WipeTowerWriter2 &writer,
+72
View File
@@ -0,0 +1,72 @@
#ifndef slic3r_LocalZOrderOptimizer_hpp_
#define slic3r_LocalZOrderOptimizer_hpp_
#include <algorithm>
#include <numeric>
#include <vector>
namespace Slic3r {
namespace LocalZOrderOptimizer {
inline bool bucket_contains_extruder(const std::vector<unsigned int> &extruders, int extruder_id)
{
return extruder_id >= 0 &&
std::find(extruders.begin(), extruders.end(), static_cast<unsigned int>(extruder_id)) != extruders.end();
}
inline std::vector<unsigned int> order_bucket_extruders(std::vector<unsigned int> extruders,
int current_extruder,
int preferred_last_extruder = -1)
{
extruders.erase(std::unique(extruders.begin(), extruders.end()), extruders.end());
if (extruders.empty())
return extruders;
if (current_extruder >= 0) {
auto current_it = std::find(extruders.begin(), extruders.end(), static_cast<unsigned int>(current_extruder));
if (current_it != extruders.end())
std::rotate(extruders.begin(), current_it, extruders.end());
}
if (preferred_last_extruder >= 0 && extruders.size() > 1 && static_cast<int>(extruders.front()) != preferred_last_extruder) {
auto preferred_it = std::find(extruders.begin() + 1, extruders.end(), static_cast<unsigned int>(preferred_last_extruder));
if (preferred_it != extruders.end())
std::rotate(preferred_it, preferred_it + 1, extruders.end());
}
return extruders;
}
inline std::vector<size_t> order_pass_group(const std::vector<std::vector<unsigned int>> &group_extruders, int current_extruder)
{
std::vector<size_t> remaining(group_extruders.size());
std::iota(remaining.begin(), remaining.end(), size_t(0));
std::vector<size_t> ordered;
ordered.reserve(group_extruders.size());
int active_extruder = current_extruder;
while (!remaining.empty()) {
auto next_it = std::find_if(remaining.begin(), remaining.end(), [&](size_t idx) {
return bucket_contains_extruder(group_extruders[idx], active_extruder);
});
if (next_it == remaining.end())
next_it = remaining.begin();
const size_t next_idx = *next_it;
ordered.push_back(next_idx);
const std::vector<unsigned int> ordered_bucket = order_bucket_extruders(group_extruders[next_idx], active_extruder);
if (!ordered_bucket.empty())
active_extruder = static_cast<int>(ordered_bucket.back());
remaining.erase(next_it);
}
return ordered;
}
} // namespace LocalZOrderOptimizer
} // namespace Slic3r
#endif
File diff suppressed because it is too large Load Diff
+312
View File
@@ -0,0 +1,312 @@
#ifndef slic3r_MixedFilament_hpp_
#define slic3r_MixedFilament_hpp_
#include <string>
#include <vector>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <utility>
namespace Slic3r {
// Represents a virtual "mixed" filament created from physical filaments
// (layer cadence and/or same-layer interleaved stripe distribution). Display
// colour blending uses FilamentMixer so pair previews better
// match expected print mixing
// (for example Blue+Yellow -> Green, Red+Yellow -> Orange, Red+Blue -> Purple).
// Legacy RYB code is retained in source for reference only.
struct MixedFilament
{
enum DistributionMode : uint8_t {
LayerCycle = 0,
SameLayerPointillisme = 1,
Simple = 2
};
// 1-based physical filament IDs that are combined.
unsigned int component_a = 1;
unsigned int component_b = 2;
// Persistent row identity used to keep painted virtual-tool assignments
// stable even when the visible mixed-filament list is rebuilt.
uint64_t stable_id = 0;
// Layer-alternation ratio. With ratio_a = 2, ratio_b = 1 the cycle is
// A, A, B, A, A, B, ...
int ratio_a = 1;
int ratio_b = 1;
// Blend percentage of component B in [0..100].
int mix_b_percent = 50;
// Optional manual pattern for this mixed filament. Tokens:
// '1' => component_a, '2' => component_b, '3'..'9' => direct physical
// filament IDs (1-based). Example: "11112222" => AAAABBBB repeating.
std::string manual_pattern;
// Optional explicit gradient multi-color component list, encoded as
// compact physical filament IDs (for example "123" -> filaments 1,2,3).
// Interleaved stripe mode is active for gradient rows only when this list has 3+ IDs.
std::string gradient_component_ids;
// Optional explicit multi-color weights aligned with gradient_component_ids.
// Compact integer list joined by '/': for example "50/25/25".
std::string gradient_component_weights;
// Legacy compatibility flag from earlier prototype serialization.
bool pointillism_all_filaments = false;
// How this mixed row is distributed:
// - LayerCycle: one filament per layer based on cadence.
// - SameLayerPointillisme: split painted masks in XY on each layer.
int distribution_mode = int(Simple);
// Optional Local-Z cap for this mixed row. 0 disables the cap.
int local_z_max_sublayers = 0;
// Additional XY surface offsets, in mm, applied when this mixed row
// resolves to component A or B for an entire layer. Positive values
// contract inward; negative values expand outward.
float component_a_surface_offset = 0.f;
float component_b_surface_offset = 0.f;
// Whether this mixed filament is enabled (available for assignment).
bool enabled = true;
// True when this mixed filament row was deleted from UI and should stay hidden.
bool deleted = false;
// True when this row was user-created (custom) instead of auto-generated.
bool custom = false;
// True when this row originated from an auto-generated pair. This remains
// true even after editing so delete logic can keep the base auto pair
// tombstoned instead of letting regeneration resurrect it.
bool origin_auto = false;
// Computed display colour as "#RRGGBB".
std::string display_color;
bool operator==(const MixedFilament &rhs) const
{
constexpr float k_surface_offset_epsilon = 1e-6f;
return component_a == rhs.component_a &&
component_b == rhs.component_b &&
stable_id == rhs.stable_id &&
ratio_a == rhs.ratio_a &&
ratio_b == rhs.ratio_b &&
mix_b_percent == rhs.mix_b_percent &&
manual_pattern == rhs.manual_pattern &&
gradient_component_ids == rhs.gradient_component_ids &&
gradient_component_weights == rhs.gradient_component_weights &&
pointillism_all_filaments == rhs.pointillism_all_filaments &&
distribution_mode == rhs.distribution_mode &&
local_z_max_sublayers == rhs.local_z_max_sublayers &&
std::abs(component_a_surface_offset - rhs.component_a_surface_offset) <= k_surface_offset_epsilon &&
std::abs(component_b_surface_offset - rhs.component_b_surface_offset) <= k_surface_offset_epsilon &&
enabled == rhs.enabled &&
deleted == rhs.deleted &&
custom == rhs.custom &&
origin_auto == rhs.origin_auto;
}
bool operator!=(const MixedFilament &rhs) const { return !(*this == rhs); }
};
struct MixedFilamentPreviewSettings
{
double nominal_layer_height { 0.2 };
double mixed_lower_bound { 0.04 };
double mixed_upper_bound { 0.16 };
double preferred_a_height { 0.0 };
double preferred_b_height { 0.0 };
bool local_z_mode { false };
bool local_z_direct_multicolor { false };
size_t wall_loops { 1 };
};
struct MixedFilamentDisplayContext
{
size_t num_physical { 0 };
std::vector<std::string> physical_colors;
std::vector<double> nozzle_diameters;
MixedFilamentPreviewSettings preview_settings;
bool component_bias_enabled { false };
};
int mixed_filament_effective_local_z_preview_mix_b_percent(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings);
bool mixed_filament_supports_bias_apparent_color(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings,
bool bias_mode_enabled);
std::pair<int, int> mixed_filament_apparent_pair_percentages(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings,
const std::vector<double> &nozzle_diameters,
bool bias_mode_enabled);
std::string compute_mixed_filament_display_color(const MixedFilament &entry, const MixedFilamentDisplayContext &context);
// ---------------------------------------------------------------------------
// MixedFilamentManager
//
// Owns the list of mixed filaments and provides helpers used by the slicing
// pipeline to resolve virtual IDs back to physical extruders.
//
// Virtual filament IDs are numbered starting at (num_physical + 1). For a
// 4-extruder printer the first mixed filament has ID 5, the second 6, etc.
// ---------------------------------------------------------------------------
class MixedFilamentManager
{
public:
MixedFilamentManager() = default;
static void set_auto_generate_enabled(bool enabled);
static bool auto_generate_enabled();
// ---- Auto-generation ------------------------------------------------
// Rebuild the mixed-filament list from the current set of physical
// filament colours. Generates all C(N,2) pairwise combinations.
// Previous ratio/enabled state is preserved when a combination still
// exists.
void auto_generate(const std::vector<std::string> &filament_colours);
// Remove a physical filament (1-based ID) from the mixed list.
// Any mixed filament that contains the removed component is deleted.
// Remaining component IDs are shifted down to stay aligned with physical IDs.
void remove_physical_filament(unsigned int deleted_filament_id);
// Add a custom mixed filament.
void add_custom_filament(unsigned int component_a, unsigned int component_b, int mix_b_percent, const std::vector<std::string> &filament_colours);
// Remove all custom rows, keep auto-generated ones.
void clear_custom_entries();
// Recompute cadence ratios from gradient settings.
// gradient_mode: 0 = Layer cycle weighted, 1 = Height weighted.
void apply_gradient_settings(int gradient_mode,
float lower_bound,
float upper_bound,
bool advanced_dithering = false);
// Persist mixed rows, including auto/deleted state, into the compact
// project-settings string.
std::string serialize_custom_entries();
void load_custom_entries(const std::string &serialized, const std::vector<std::string> &filament_colours);
// Normalize a manual mixed-pattern string into compact token form.
// Accepts separators and A/B aliases. Returns empty string if invalid.
static std::string normalize_manual_pattern(const std::string &pattern);
static int mix_percent_from_manual_pattern(const std::string &pattern);
// ---- Queries --------------------------------------------------------
// True when `filament_id` (1-based) refers to a mixed filament.
bool is_mixed(unsigned int filament_id, size_t num_physical) const
{
return mixed_index_from_filament_id(filament_id, num_physical) >= 0;
}
// Resolve a mixed filament ID to a physical extruder (1-based) for the
// given layer context. Returns `filament_id` unchanged when it is not a
// mixed filament.
unsigned int resolve(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z = 0.f,
float layer_height = 0.f,
bool force_height_weighted = false) const;
unsigned int resolve_perimeter(unsigned int filament_id,
size_t num_physical,
int layer_index,
int perimeter_index,
float layer_print_z = 0.f,
float layer_height = 0.f,
bool force_height_weighted = false) const;
// Resolve the filament ID that should own painted regions on this layer.
// Modes that require virtual identity later in G-code generation keep the
// original mixed ID; ordinary mixed rows collapse to the current physical
// extruder so adjacent same-tool regions can merge.
unsigned int effective_painted_region_filament_id(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z = 0.f,
float layer_height = 0.f,
float layer_height_a = 0.f,
float layer_height_b = 0.f,
float base_layer_height = 0.2f) const;
float component_surface_offset(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z = 0.f,
float layer_height = 0.f,
bool force_height_weighted = false) const;
std::vector<unsigned int> ordered_perimeter_extruders(unsigned int filament_id,
size_t num_physical,
int layer_index,
float layer_print_z = 0.f,
float layer_height = 0.f,
bool force_height_weighted = false) const;
// Map virtual filament ID (1-based, after physical IDs) to index into
// m_mixed. Virtual IDs enumerate enabled mixed rows only.
int mixed_index_from_filament_id(unsigned int filament_id, size_t num_physical) const;
// Blend N colours using weighted FilamentMixer blending.
// color_percents: vector of (hex_color, percent) where percents sum to 100.
static std::string blend_color_multi(
const std::vector<std::pair<std::string, int>> &color_percents);
const MixedFilament *mixed_filament_from_id(unsigned int filament_id, size_t num_physical) const;
// Compute a display colour by blending two colours with FilamentMixer.
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,
float reference_width_mm = 0.4f);
// ---- Accessors ------------------------------------------------------
const std::vector<MixedFilament> &mixed_filaments() const { return m_mixed; }
std::vector<MixedFilament> &mixed_filaments() { return m_mixed; }
size_t enabled_count() const;
// Total filament count = num_physical + number of *enabled* mixed filaments.
size_t total_filaments(size_t num_physical) const { return num_physical + enabled_count(); }
// Return the display colours of all enabled mixed filaments (in order).
std::vector<std::string> display_colors() const;
void set_display_context(const MixedFilamentDisplayContext &context);
private:
// Convert a 1-based virtual ID to a 0-based index into m_mixed.
size_t index_of(unsigned int filament_id, size_t num_physical) const
{
return static_cast<size_t>(filament_id - num_physical - 1);
}
void refresh_display_colors(const std::vector<std::string> &filament_colours);
uint64_t allocate_stable_id();
uint64_t normalize_stable_id(uint64_t stable_id);
std::vector<MixedFilament> m_mixed;
int m_gradient_mode = 0;
float m_height_lower_bound = 0.04f;
float m_height_upper_bound = 0.16f;
bool m_advanced_dithering = false;
uint64_t m_next_stable_id = 1;
MixedFilamentDisplayContext m_display_context;
};
} // namespace Slic3r
#endif /* slic3r_MixedFilament_hpp_ */
+22 -2
View File
@@ -2575,15 +2575,26 @@ void ModelVolume::update_extruder_count(size_t extruder_count)
}
}
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id)
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id,
const std::vector<unsigned char>& filament_is_mixed)
{
std::vector<int> used_extruders = get_extruders();
for (int extruder_id : used_extruders) {
if (extruder_id >= filament_id) {
if (extruder_id >= (int)filament_id) {
mmu_segmentation_facets.set_enforcer_block_type_limit(*this, (EnforcerBlockerType)(extruder_count), (EnforcerBlockerType)(filament_id), (EnforcerBlockerType)(replace_filament_id));
break;
}
}
// Skip erasing the volume's extruder config if it points at a mixed slot —
// mixed slots remain valid even after a physical filament deletion.
size_t eid = (size_t)extruder_id();
if (eid > extruder_count) {
bool is_mixed = !filament_is_mixed.empty() && eid >= 1
&& (eid - 1) < filament_is_mixed.size()
&& filament_is_mixed[eid - 1];
if (!is_mixed)
this->config.erase("extruder");
}
}
void ModelVolume::center_geometry_after_creation(bool update_source_offset)
@@ -3521,6 +3532,15 @@ bool FacetsAnnotation::set(const TriangleSelector& selector)
return false;
}
void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta)
{
if (empty()) return;
TriangleSelector selector(mv.mesh());
selector.deserialize(m_data, false);
selector.shift_states_above(threshold, delta);
this->set(selector);
}
void FacetsAnnotation::reset()
{
m_data.triangles_to_split.clear();
+5 -1
View File
@@ -742,6 +742,9 @@ public:
EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE);
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
// Shift all non-NONE leaf states >= threshold by delta.
// Used to renumber painted filament IDs after a filament slot insertion/deletion.
void shift_states_above(const ModelVolume& mv, EnforcerBlockerType threshold, int delta);
bool empty() const { return m_data.triangles_to_split.empty(); }
// Following method clears the config and increases its timestamp, so the deleted
@@ -925,7 +928,8 @@ public:
// BBS
std::vector<int> get_extruders() const;
void update_extruder_count(size_t extruder_count);
void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1);
void update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id = -1,
const std::vector<unsigned char>& filament_is_mixed = {});
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
+6 -1
View File
@@ -2195,7 +2195,12 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
// Returns multi-material segmentation based on painting in multi-material segmentation gizmo
std::vector<std::vector<ExPolygons>> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback) {
const size_t num_facets_states = print_object.print()->config().filament_colour.size() + 1;
const size_t num_physical_filaments = print_object.print()->config().filament_colour.size();
// Virtual (mixed) filament IDs are opaque integers in the range
// (num_physical, total_filaments]. The segmentation pipeline treats
// all filament IDs as opaque; no assert on id <= num_physical is needed.
const size_t num_total_filaments = print_object.print()->mixed_filament_manager().total_filaments(num_physical_filaments);
const size_t num_facets_states = num_total_filaments + 1;
const float max_width = float(print_object.config().mmu_segmented_region_max_width.value);
const float interlocking_depth = float(print_object.config().mmu_segmented_region_interlocking_depth.value);
const bool interlocking_beam = print_object.config().interlocking_beam.value;
+20
View File
@@ -1264,6 +1264,26 @@ static std::vector<std::string> s_Preset_print_options{
"zaa_dont_alternate_fill_direction",
"zaa_min_z",
"ironing_expansion",
// Mixed-filament + dithering + infill-override keys
"enable_infill_filament_override",
"infill_filament_use_base_first_layers",
"infill_filament_use_base_last_layers",
"mixed_color_layer_height_a",
"mixed_color_layer_height_b",
"mixed_filament_gradient_mode",
"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",
"dithering_z_step_size",
"dithering_local_z_mode",
"dithering_local_z_whole_objects",
"dithering_local_z_direct_multicolor",
"dithering_step_painted_zones_only",
"local_z_wipe_tower_purge_lines"
};
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
+208 -3
View File
@@ -10,9 +10,11 @@
#include "libslic3r_version.h"
#include <algorithm>
#include <map>
#include <mutex>
#include <set>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
#include <boost/filesystem.hpp>
#include <boost/algorithm/clamp.hpp>
@@ -53,7 +55,9 @@ static std::vector<std::string> s_project_options {
"flush_multiplier",
"nozzle_volume_type",
"filament_map_mode",
"filament_map"
"filament_map",
// FullSpectrum: mixed filament definitions string (virtual slot configuration)
"mixed_filament_definitions"
};
//Orca: add custom as default
@@ -2682,6 +2686,7 @@ void PresetBundle::update_selections(AppConfig &config)
// exist.
this->update_compatible(PresetSelectCompatibleType::Always);
this->update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
std::string first_visible_filament_name;
for (auto & fp : filament_presets) {
@@ -2820,12 +2825,20 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
project_config.option<ConfigOptionFloats>("flush_multiplier")->values = std::vector<double>(flush_multipliers.begin(), flush_multipliers.end());
}
// Restore mixed filament definitions persisted across sessions.
if (config.has("presets", "mixed_filament_definitions")) {
auto *defs = project_config.option<ConfigOptionString>("mixed_filament_definitions");
if (defs)
defs->value = config.get("presets", "mixed_filament_definitions");
}
// Update visibility of presets based on their compatibility with the active printer.
// Always try to select a compatible print and filament preset to the current printer preset,
// as the application may have been closed with an active "external" preset, which does not
// exist.
this->update_compatible(PresetSelectCompatibleType::Always);
this->update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
if (initial_printer != nullptr && (preferred_printer == nullptr || initial_printer == preferred_printer)) {
// Don't run the following code, as we want to activate default filament / SLA material profiles when installing and selecting a new printer.
@@ -2953,6 +2966,11 @@ void PresetBundle::export_selections(AppConfig &config)
"|");
config.set_printer_setting(printer_name, "flush_multiplier", flush_multiplier_str);
// Persist mixed filament definitions across sessions.
sync_mixed_filaments_to_config();
if (auto *defs = project_config.option<ConfigOptionString>("mixed_filament_definitions"))
config.set("presets", "mixed_filament_definitions", defs->value);
// BBS
//config.set("presets", "sla_print", sla_prints.get_selected_preset_name());
//config.set("presets", "sla_material", sla_materials.get_selected_preset_name());
@@ -2997,6 +3015,7 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> ne
}
update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
}
void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
{
@@ -3034,6 +3053,26 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
}
update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
}
void PresetBundle::sync_mixed_filaments_from_config()
{
auto *col_opt = project_config.option<ConfigOptionStrings>("filament_colour");
auto *defs_opt = project_config.option<ConfigOptionString>("mixed_filament_definitions");
if (!col_opt)
return;
mixed_filaments.auto_generate(col_opt->values);
if (defs_opt && !defs_opt->value.empty())
mixed_filaments.load_custom_entries(defs_opt->value, col_opt->values);
}
void PresetBundle::sync_mixed_filaments_to_config()
{
auto *defs_opt = project_config.option<ConfigOptionString>("mixed_filament_definitions");
if (!defs_opt)
return;
defs_opt->value = mixed_filaments.serialize_custom_entries();
}
void PresetBundle::update_num_filaments(unsigned int to_del_flament_id)
@@ -3084,6 +3123,11 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id)
erase_or_resize(filament_color_type->values);
erase_or_resize(ams_multi_color_filment);
// Remove any virtual mixed rows that contained this physical filament,
// then persist the updated definitions back into project_config.
mixed_filaments.remove_physical_filament(to_del_flament_id + 1); // 1-based
sync_mixed_filaments_to_config();
update_multi_material_filament_presets(to_del_flament_id);
}
@@ -3143,6 +3187,7 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfig *,std::string>> &unknowns, bool use_map, std::map<int, AMSMapInfo> &maps, bool enable_append, MergeFilamentInfo &merge_info, bool color_only)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "use_map:" << use_map << " enable_append:" << enable_append;
std::vector<std::string> ams_filament_presets;
std::vector<std::string> ams_filament_colors;
std::vector<std::string> ams_filament_color_types;
@@ -3287,6 +3332,14 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
}
if (ams_filament_presets.empty())
return 0;
// AMS sync must not clobber mixed (virtual) filament data — save and restore.
// Guard is placed here (after the early-return) so no early-return path bypasses
// the restore block below.
auto saved_mixed = mixed_filaments;
auto *defs_opt_ams = project_config.option<ConfigOptionString>("mixed_filament_definitions");
std::string saved_defs = defs_opt_ams ? defs_opt_ams->value : std::string{};
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "get filament_colour and from config";
ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
@@ -3543,6 +3596,12 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
if (support_interface_filament_opt->value > filament_color_type->values.size())
support_interface_filament_opt->value = 0;
}
// Restore mixed (virtual) filament data that AMS sync must not overwrite.
mixed_filaments = saved_mixed;
if (defs_opt_ams)
defs_opt_ams->value = saved_defs;
sync_mixed_filaments_from_config();
// Update ams_multi_color_filment
update_filament_multi_color();
update_multi_material_filament_presets();
@@ -4557,6 +4616,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
this->update_compatible(PresetSelectCompatibleType::Never);
this->update_multi_material_filament_presets();
// FullSpectrum: rebuild the MixedFilamentManager from the just-loaded project config.
// Per-row warnings for skipped invalid entries are emitted by load_custom_entries.
sync_mixed_filaments_from_config();
//BBS
//const std::string &physical_printer = config.option<ConfigOptionString>("physical_printer_settings_id", true)->value;
const std::string physical_printer;
@@ -5116,7 +5179,7 @@ void PresetBundle::on_extruders_count_changed(int extruders_count)
extruder_ams_counts.resize(extruders_count);
}
void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filament_id)
void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filament_id, size_t old_num_filaments_arg)
{
if (printers.get_edited_preset().printer_technology() != ptFFF)
return;
@@ -5127,6 +5190,13 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
// Verify and select the filament presets.
size_t num_filaments = this->filament_presets.size();
const bool deleting_filament = (to_delete_filament_id != size_t(-1));
const size_t old_num_filaments = (old_num_filaments_arg != size_t(-1))
? old_num_filaments_arg
: (deleting_filament ? (num_filaments + 1) : num_filaments);
const std::vector<MixedFilament> old_mixed = this->mixed_filaments.mixed_filaments();
m_last_filament_id_remap.clear();
auto* nozzle_diameter = static_cast<const ConfigOptionFloats*>(printers.get_edited_preset().config.option("nozzle_diameter"));
size_t num_extruders = nozzle_diameter->values.size();
if (num_extruders > num_filaments) { // Verify validity of the current filament presets.
@@ -5137,7 +5207,8 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
this->filament_presets.back());
num_filaments = this->filament_presets.size();
}
if (to_delete_filament_id == -1)
if (!deleting_filament)
to_delete_filament_id = num_filaments;
// Now verify if flush_volumes_matrix has proper size (it is used to deduce number of extruders in wipe tower generator):
@@ -5188,6 +5259,140 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
}
this->project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values = new_matrix;
}
// Build old->new filament ID remap for painted facet data normalization.
// This is needed for both deletion and addition of physical filaments so
// painted mixed states keep pointing at the same virtual mixed entries.
if (old_num_filaments != num_filaments || deleting_filament || old_mixed != this->mixed_filaments.mixed_filaments())
build_filament_id_remap(old_mixed, old_num_filaments, num_filaments, deleting_filament,
deleting_filament ? unsigned(to_delete_filament_id + 1) : 0u);
}
void PresetBundle::update_mixed_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments)
{
build_filament_id_remap(old_mixed, old_num_filaments, new_num_filaments, false, 0u);
}
void PresetBundle::build_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments,
bool deleting_filament,
unsigned int deleted_1based)
{
size_t old_enabled_mixed = 0;
for (const auto &mf : old_mixed)
if (mf.enabled)
++old_enabled_mixed;
const size_t old_total_filaments = old_num_filaments + old_enabled_mixed;
m_last_filament_id_remap.assign(old_total_filaments + 1, 0);
for (unsigned int old_id = 1; old_id <= unsigned(old_num_filaments); ++old_id) {
unsigned int mapped = 0;
if (deleting_filament && old_id == deleted_1based) {
mapped = 0;
} else if (old_id <= unsigned(new_num_filaments)) {
mapped = old_id;
if (deleting_filament && old_id > deleted_1based)
--mapped;
}
m_last_filament_id_remap[old_id] = mapped;
}
auto canonical_pair = [](unsigned int a, unsigned int b) {
return std::make_pair(std::min(a, b), std::max(a, b));
};
std::unordered_map<uint64_t, unsigned int> new_stable_id_to_virtual_id;
std::map<std::pair<unsigned int, unsigned int>, std::vector<unsigned int>> new_pair_to_ids;
unsigned int next_virtual_id = unsigned(new_num_filaments + 1);
for (const auto &mf : this->mixed_filaments.mixed_filaments()) {
if (!mf.enabled)
continue;
if (mf.stable_id != 0)
new_stable_id_to_virtual_id.emplace(mf.stable_id, next_virtual_id);
new_pair_to_ids[canonical_pair(mf.component_a, mf.component_b)].push_back(next_virtual_id++);
}
std::map<std::pair<unsigned int, unsigned int>, size_t> used_per_pair;
size_t stable_id_hits = 0;
size_t fallback_pair_hits = 0;
size_t missing_hits = 0;
unsigned int old_virtual_id = unsigned(old_num_filaments + 1);
for (const auto &mf : old_mixed) {
if (!mf.enabled)
continue;
unsigned int a = mf.component_a;
unsigned int b = mf.component_b;
if (a == deleted_1based || b == deleted_1based) {
m_last_filament_id_remap[old_virtual_id] = 0;
++missing_hits;
} else {
bool mapped_by_stable_id = false;
if (mf.stable_id != 0) {
auto it_stable = new_stable_id_to_virtual_id.find(mf.stable_id);
if (it_stable != new_stable_id_to_virtual_id.end()) {
m_last_filament_id_remap[old_virtual_id] = it_stable->second;
mapped_by_stable_id = true;
++stable_id_hits;
}
}
if (!mapped_by_stable_id) {
if (deleting_filament) {
if (a > deleted_1based)
--a;
if (b > deleted_1based)
--b;
}
const auto key = canonical_pair(a, b);
auto it = new_pair_to_ids.find(key);
if (it == new_pair_to_ids.end()) {
m_last_filament_id_remap[old_virtual_id] = 0;
++missing_hits;
} else {
size_t &used = used_per_pair[key];
if (used >= it->second.size()) {
m_last_filament_id_remap[old_virtual_id] = 0;
++missing_hits;
} else {
m_last_filament_id_remap[old_virtual_id] = it->second[used++];
++fallback_pair_hits;
}
}
}
}
++old_virtual_id;
}
auto summarize_uint_vector = [](const std::vector<unsigned int> &values, size_t max_items = 24) {
std::string out = "[";
const size_t n = std::min(values.size(), max_items);
for (size_t i = 0; i < n; ++i) {
if (i > 0)
out += ",";
out += std::to_string(values[i]);
}
if (values.size() > n)
out += ",...";
out += "]";
return out;
};
BOOST_LOG_TRIVIAL(warning) << "MF_REMAP preset_bundle"
<< " old_physical=" << old_num_filaments
<< " new_physical=" << new_num_filaments
<< " deleting=" << (deleting_filament ? 1 : 0)
<< " deleted_id=" << deleted_1based
<< " old_mixed_enabled=" << old_enabled_mixed
<< " new_mixed_enabled=" << this->mixed_filaments.enabled_count()
<< " stable_id_hits=" << stable_id_hits
<< " fallback_pair_hits=" << fallback_pair_hits
<< " missing_hits=" << missing_hits
<< " remap_size=" << m_last_filament_id_remap.size()
<< " remap=" << summarize_uint_vector(m_last_filament_id_remap);
}
void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible)
+42 -1
View File
@@ -4,6 +4,7 @@
#include "Preset.hpp"
#include "AppConfig.hpp"
#include "enum_bitmask.hpp"
#include "MixedFilament.hpp"
#include <memory>
#include <shared_mutex>
@@ -289,6 +290,19 @@ public:
//BBS: check whether this is the only edited filament
bool is_the_only_edited_filament(unsigned int filament_index);
// Mixed filament helpers — query virtual slot info
bool is_mixed_filament(size_t idx) const {
return mixed_filaments.is_mixed(static_cast<unsigned int>(idx + 1),
filament_presets.size());
}
size_t total_filament_count() const {
return mixed_filaments.total_filaments(filament_presets.size());
}
// Sync the MixedFilamentManager to/from the project_config string key.
void sync_mixed_filaments_from_config();
void sync_mixed_filaments_to_config();
void reset_default_nozzle_volume_type();
std::vector<int> get_used_tpu_filaments(const std::vector<int> &used_filaments);
@@ -326,6 +340,10 @@ public:
std::map<int, DynamicPrintConfig> filament_ams_list;
std::vector<std::vector<std::string>> ams_multi_color_filment;
// Mixed (virtual) filaments for layer-based colour mixing.
// This is the canonical instance; Print::m_mixed_filament_mgr is a slicing-time copy.
MixedFilamentManager mixed_filaments;
std::vector<std::map<int, int>> extruder_ams_counts;
// Calibrate
@@ -436,7 +454,23 @@ public:
// Read out the number of extruders from an active printer preset,
// update size and content of filament_presets.
void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1));
// old_num_filaments: physical filament count before any add/delete (size_t(-1) = auto-detect).
void update_multi_material_filament_presets(size_t to_delete_filament_id = size_t(-1),
size_t old_num_filaments = size_t(-1));
// Rebuild old->new virtual filament mapping after mixed-row enable/delete
// changes when the physical filament count itself did not change.
void update_mixed_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments);
// Mapping generated during the latest filament count change.
// Index is old 1-based filament ID, value is new 1-based filament ID (0 = removed).
const std::vector<unsigned int>& last_filament_id_remap() const { return m_last_filament_id_remap; }
std::vector<unsigned int> consume_last_filament_id_remap()
{
std::vector<unsigned int> out = std::move(m_last_filament_id_remap);
m_last_filament_id_remap.clear();
return out;
}
void on_extruders_count_changed(int extruder_count);
@@ -493,6 +527,12 @@ private:
void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles.
void update_system_maps();
// Build old->new filament ID remap for painted facet data normalization.
void build_filament_id_remap(const std::vector<MixedFilament> &old_mixed,
size_t old_num_filaments,
size_t new_num_filaments,
bool deleting_filament,
unsigned int deleted_1based);
// Set the is_visible flag for filaments and sla materials,
// apply defaults based on enabled printers when no filaments/materials are installed.
@@ -513,6 +553,7 @@ private:
bool validation_mode = false;
std::string vendor_to_validate = "";
int m_errors = 0;
std::vector<unsigned int> m_last_filament_id_remap;
// Helper function: save preset to bundle directory with common logic
bool save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection,
+584 -6
View File
@@ -8,6 +8,7 @@
#include "Flow.hpp"
#include "Geometry/ConvexHull.hpp"
#include "I18N.hpp"
#include "LocalZOrderOptimizer.hpp"
#include "ShortestPath.hpp"
#include "Thread.hpp"
#include "Time.hpp"
@@ -17,6 +18,7 @@
#include "Utils.hpp"
#include "PrintConfig.hpp"
#include "MaterialType.hpp"
#include "MixedFilament.hpp"
#include "Model.hpp"
#include "format.hpp"
#include <float.h>
@@ -54,6 +56,489 @@ template class PrintState<PrintObjectStep, posCount>;
PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {}
PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(config), config.hash()) {}
// Estimate how many Local-Z unplanned wipe-tower reserve slots are needed for
// a given print layer height (used when building the wipe tower plan).
static size_t estimate_local_z_wipe_tower_reserve_slots(const PrintObject& print_object, coordf_t print_z)
{
const Layer* object_layer = print_object.get_layer_at_printz(print_z, EPSILON);
if (object_layer == nullptr)
return 0;
const auto& intervals = print_object.local_z_intervals();
const auto& plans = print_object.local_z_sublayer_plan();
if (intervals.empty() || plans.empty())
return 0;
const size_t layer_id = size_t(object_layer->id());
const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval& interval) {
return interval.layer_id == layer_id;
});
if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 ||
interval_it->first_sublayer_idx >= plans.size()) {
return 0;
}
const size_t first_idx = interval_it->first_sublayer_idx;
const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count);
size_t reserve_slots = 0;
int previous_extruder = -1;
for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) {
const SubLayerPlan& plan = plans[plan_idx];
if (!plan.split_interval)
continue;
for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) {
if (plan.painted_masks_by_extruder[extruder_id].empty())
continue;
if (previous_extruder != int(extruder_id)) {
++reserve_slots;
previous_extruder = int(extruder_id);
}
}
}
if (reserve_slots > 0)
++reserve_slots;
return reserve_slots;
}
namespace {
constexpr double LOCAL_Z_PERIMETER_MASK_EXPAND_MM = 0.10;
struct LocalZWipeTowerToolchange
{
unsigned int old_tool { 0 };
unsigned int new_tool { 0 };
};
struct LocalZWipeTowerPassRef
{
size_t layer_to_print_idx { 0 };
const SubLayerPlan *plan { nullptr };
std::vector<unsigned int> extruders;
};
static inline ExPolygons local_z_compensate_masks_for_wipe_tower(const ExPolygons &src_masks,
const float delta_scaled,
const bool fallback_to_source)
{
if (src_masks.empty() || std::abs(delta_scaled) <= EPSILON)
return src_masks;
ExPolygons compensated = offset_ex(src_masks, delta_scaled);
if (!compensated.empty() && compensated.size() > 1)
compensated = union_ex(compensated);
if (compensated.empty() && fallback_to_source)
return src_masks;
return compensated;
}
static bool local_z_segments_exist(Polylines segments)
{
for (Polyline &segment : segments) {
if (segment.is_valid())
return true;
}
return false;
}
static bool extrusion_collection_has_local_z_perimeter_segment(const ExtrusionEntityCollection &source,
const ExPolygons &include_masks)
{
if (source.entities.empty() || include_masks.empty())
return false;
ExtrusionEntityCollection flattened = source.flatten(false);
for (const ExtrusionEntity *entity : flattened.entities) {
if (const auto *path = dynamic_cast<const ExtrusionPath*>(entity)) {
if (local_z_segments_exist(intersection_pl(Polylines{path->polyline}, include_masks)))
return true;
} else if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath*>(entity)) {
for (const ExtrusionPath &path : multipath->paths) {
if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks)))
return true;
}
} else if (const auto *loop = dynamic_cast<const ExtrusionLoop*>(entity)) {
for (const ExtrusionPath &path : loop->paths) {
if (local_z_segments_exist(intersection_pl(Polylines{path.polyline}, include_masks)))
return true;
}
}
}
return false;
}
static bool layer_has_local_z_perimeters(const Layer &layer, const ExPolygons &pass_masks)
{
if (pass_masks.empty())
return false;
for (const LayerRegion *layer_region : layer.regions()) {
for (const ExtrusionEntity *entity : layer_region->perimeters.entities) {
const auto *extrusions = dynamic_cast<const ExtrusionEntityCollection*>(entity);
if (extrusions == nullptr)
continue;
if (extrusion_collection_has_local_z_perimeter_segment(*extrusions, pass_masks))
return true;
}
}
return false;
}
static inline int shared_local_z_extruder_for_wipe_tower(const std::vector<unsigned int> &lhs,
const std::vector<unsigned int> &rhs)
{
for (unsigned int extruder_id : lhs) {
if (std::find(rhs.begin(), rhs.end(), extruder_id) != rhs.end())
return static_cast<int>(extruder_id);
}
return -1;
}
static std::vector<unsigned int> rotate_extruders_to_start_with(const std::vector<unsigned int> &extruders,
unsigned int start_extruder)
{
std::vector<unsigned int> rotated = extruders;
auto it = std::find(rotated.begin(), rotated.end(), start_extruder);
if (it != rotated.end())
std::rotate(rotated.begin(), it, rotated.end());
return rotated;
}
static std::vector<LocalZWipeTowerToolchange> collect_local_z_wipe_tower_toolchanges(
const Print &print,
const std::vector<GCode::LayerToPrint> &layers,
int start_extruder)
{
std::vector<LocalZWipeTowerPassRef> pass_refs;
const bool local_z_whole_objects_enabled = print.full_print_config().opt_bool("dithering_local_z_whole_objects");
const float local_z_perimeter_mask_expand = float(scale_(LOCAL_Z_PERIMETER_MASK_EXPAND_MM));
for (size_t layer_to_print_idx = 0; layer_to_print_idx < layers.size(); ++layer_to_print_idx) {
const GCode::LayerToPrint &layer_to_print = layers[layer_to_print_idx];
if (layer_to_print.object_layer == nullptr)
continue;
const PrintObject *print_object =
layer_to_print.original_object != nullptr ? layer_to_print.original_object : layer_to_print.object();
if (print_object == nullptr)
continue;
const size_t layer_id = size_t(layer_to_print.object_layer->id());
const auto &intervals = print_object->local_z_intervals();
const auto &plans = print_object->local_z_sublayer_plan();
const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval &interval) {
return interval.layer_id == layer_id;
});
if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 ||
interval_it->first_sublayer_idx >= plans.size()) {
continue;
}
const size_t first_idx = interval_it->first_sublayer_idx;
const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count);
for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) {
const SubLayerPlan &plan = plans[plan_idx];
if (!plan.split_interval)
continue;
const size_t plan_mask_slots =
std::max(plan.painted_masks_by_extruder.size(), plan.fixed_painted_masks_by_extruder.size());
std::vector<ExPolygons> compensated_masks_by_extruder(plan_mask_slots, ExPolygons());
ExPolygons fixed_raw_masks_union;
for (const ExPolygons &fixed_masks : plan.fixed_painted_masks_by_extruder) {
if (!fixed_masks.empty())
append(fixed_raw_masks_union, fixed_masks);
}
if (!fixed_raw_masks_union.empty() && fixed_raw_masks_union.size() > 1)
fixed_raw_masks_union = union_ex(fixed_raw_masks_union);
const ExPolygons fixed_compensated_guard =
fixed_raw_masks_union.empty() ?
ExPolygons() :
local_z_compensate_masks_for_wipe_tower(fixed_raw_masks_union, local_z_perimeter_mask_expand, true);
for (size_t extruder_id = 0; extruder_id < plan_mask_slots; ++extruder_id) {
const ExPolygons mixed_raw_masks =
extruder_id < plan.painted_masks_by_extruder.size() ? plan.painted_masks_by_extruder[extruder_id] : ExPolygons();
const ExPolygons fixed_raw_masks =
extruder_id < plan.fixed_painted_masks_by_extruder.size() ? plan.fixed_painted_masks_by_extruder[extruder_id] :
ExPolygons();
if (mixed_raw_masks.empty() && fixed_raw_masks.empty())
continue;
ExPolygons compensated;
if (!mixed_raw_masks.empty()) {
ExPolygons compensated_mixed =
local_z_compensate_masks_for_wipe_tower(mixed_raw_masks, local_z_perimeter_mask_expand, true);
if (local_z_whole_objects_enabled && !fixed_compensated_guard.empty())
compensated_mixed = diff_ex(compensated_mixed, fixed_compensated_guard);
if (!compensated_mixed.empty())
append(compensated, compensated_mixed);
}
if (!fixed_raw_masks.empty())
append(compensated, fixed_raw_masks);
if (!compensated.empty() && compensated.size() > 1)
compensated = union_ex(compensated);
compensated_masks_by_extruder[extruder_id] = std::move(compensated);
}
LocalZWipeTowerPassRef pass_ref;
pass_ref.layer_to_print_idx = layer_to_print_idx;
pass_ref.plan = &plan;
for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) {
if (extruder_id >= compensated_masks_by_extruder.size())
continue;
const ExPolygons &pass_masks = compensated_masks_by_extruder[extruder_id];
if (pass_masks.empty())
continue;
if (layer_has_local_z_perimeters(*layer_to_print.object_layer, pass_masks))
pass_ref.extruders.push_back(unsigned(extruder_id));
}
if (!pass_ref.extruders.empty())
pass_refs.emplace_back(std::move(pass_ref));
}
}
std::sort(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &lhs, const LocalZWipeTowerPassRef &rhs) {
assert(lhs.plan != nullptr && rhs.plan != nullptr);
if (lhs.plan->print_z != rhs.plan->print_z)
return lhs.plan->print_z < rhs.plan->print_z;
if (lhs.layer_to_print_idx != rhs.layer_to_print_idx)
return lhs.layer_to_print_idx < rhs.layer_to_print_idx;
return lhs.plan->pass_index < rhs.plan->pass_index;
});
auto collect_toolchanges_legacy = [&](int start_tool) {
std::vector<LocalZWipeTowerToolchange> legacy_toolchanges;
int active_extruder = start_tool;
size_t pass_ref_idx = 0;
while (pass_ref_idx < pass_refs.size()) {
size_t pass_group_end = pass_ref_idx + 1;
while (pass_group_end < pass_refs.size() &&
std::abs(pass_refs[pass_ref_idx].plan->print_z - pass_refs[pass_group_end].plan->print_z) <= EPSILON) {
++pass_group_end;
}
std::vector<unsigned int> pass_group_extruders;
for (size_t group_idx = pass_ref_idx; group_idx < pass_group_end; ++group_idx)
for (unsigned int extruder_id : pass_refs[group_idx].extruders)
if (std::find(pass_group_extruders.begin(), pass_group_extruders.end(), extruder_id) == pass_group_extruders.end())
pass_group_extruders.push_back(extruder_id);
std::vector<unsigned int> next_group_extruders;
if (pass_group_end < pass_refs.size()) {
size_t next_group_end = pass_group_end + 1;
while (next_group_end < pass_refs.size() &&
std::abs(pass_refs[pass_group_end].plan->print_z - pass_refs[next_group_end].plan->print_z) <= EPSILON) {
++next_group_end;
}
for (size_t group_idx = pass_group_end; group_idx < next_group_end; ++group_idx)
for (unsigned int extruder_id : pass_refs[group_idx].extruders)
if (std::find(next_group_extruders.begin(), next_group_extruders.end(), extruder_id) == next_group_extruders.end())
next_group_extruders.push_back(extruder_id);
}
const int preferred_last_extruder =
shared_local_z_extruder_for_wipe_tower(pass_group_extruders, next_group_extruders);
const std::vector<unsigned int> ordered_group_extruders =
LocalZOrderOptimizer::order_bucket_extruders(pass_group_extruders, active_extruder, preferred_last_extruder);
for (unsigned int extruder_id : ordered_group_extruders) {
if (active_extruder >= 0 && active_extruder != int(extruder_id))
legacy_toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), extruder_id});
active_extruder = int(extruder_id);
}
pass_ref_idx = pass_group_end;
}
return legacy_toolchanges;
};
const bool dependency_chain_mode =
!pass_refs.empty() &&
std::all_of(pass_refs.begin(), pass_refs.end(), [](const LocalZWipeTowerPassRef &pass_ref) {
return pass_ref.plan != nullptr && pass_ref.plan->dependency_group != 0;
});
if (!dependency_chain_mode)
return collect_toolchanges_legacy(start_extruder);
struct ChainKey {
size_t layer_to_print_idx { 0 };
size_t dependency_group { 0 };
bool operator<(const ChainKey &rhs) const
{
if (layer_to_print_idx != rhs.layer_to_print_idx)
return layer_to_print_idx < rhs.layer_to_print_idx;
return dependency_group < rhs.dependency_group;
}
};
struct PassState {
const LocalZWipeTowerPassRef *pass_ref { nullptr };
std::vector<unsigned int> remaining_extruders;
size_t chain_idx { 0 };
size_t chain_pos { 0 };
bool ready { false };
bool completed { false };
};
std::map<ChainKey, size_t> chain_index_by_key;
std::vector<std::vector<size_t>> chains;
std::vector<PassState> pass_states;
pass_states.reserve(pass_refs.size());
for (const LocalZWipeTowerPassRef &pass_ref : pass_refs) {
ChainKey chain_key { pass_ref.layer_to_print_idx, pass_ref.plan->dependency_group };
auto [it_chain, inserted] = chain_index_by_key.emplace(chain_key, chains.size());
if (inserted)
chains.emplace_back();
const size_t chain_idx = it_chain->second;
const size_t pass_state_idx = pass_states.size();
pass_states.push_back(PassState{ &pass_ref, pass_ref.extruders, chain_idx, 0, false, false });
chains[chain_idx].push_back(pass_state_idx);
}
for (std::vector<size_t> &chain : chains) {
std::sort(chain.begin(), chain.end(), [&pass_states](size_t lhs_idx, size_t rhs_idx) {
const SubLayerPlan &lhs = *pass_states[lhs_idx].pass_ref->plan;
const SubLayerPlan &rhs = *pass_states[rhs_idx].pass_ref->plan;
if (lhs.dependency_order != rhs.dependency_order)
return lhs.dependency_order < rhs.dependency_order;
if (std::abs(lhs.print_z - rhs.print_z) > EPSILON)
return lhs.print_z < rhs.print_z;
return lhs.pass_index < rhs.pass_index;
});
for (size_t chain_pos = 0; chain_pos < chain.size(); ++chain_pos)
pass_states[chain[chain_pos]].chain_pos = chain_pos;
if (!chain.empty())
pass_states[chain.front()].ready = true;
}
auto pass_contains_extruder = [](const PassState &pass_state, unsigned int extruder_id) {
return std::find(pass_state.remaining_extruders.begin(), pass_state.remaining_extruders.end(), extruder_id) !=
pass_state.remaining_extruders.end();
};
auto choose_ready_extruder = [&](int active_extruder) -> int {
std::vector<unsigned int> ready_extruders;
for (const PassState &pass_state : pass_states) {
if (!pass_state.ready || pass_state.completed)
continue;
for (unsigned int extruder_id : pass_state.remaining_extruders)
if (std::find(ready_extruders.begin(), ready_extruders.end(), extruder_id) == ready_extruders.end())
ready_extruders.push_back(extruder_id);
}
if (ready_extruders.empty())
return -1;
if (active_extruder >= 0 &&
std::find(ready_extruders.begin(), ready_extruders.end(), unsigned(active_extruder)) != ready_extruders.end()) {
return active_extruder;
}
int best_extruder = -1;
size_t best_ready_count = 0;
size_t best_future_count = 0;
for (unsigned int extruder_id : ready_extruders) {
size_t ready_count = 0;
size_t future_count = 0;
for (const PassState &pass_state : pass_states) {
if (pass_state.completed || !pass_contains_extruder(pass_state, extruder_id))
continue;
++future_count;
if (pass_state.ready)
++ready_count;
}
if (best_extruder < 0 ||
ready_count > best_ready_count ||
(ready_count == best_ready_count && future_count > best_future_count) ||
(ready_count == best_ready_count && future_count == best_future_count && extruder_id < unsigned(best_extruder))) {
best_extruder = int(extruder_id);
best_ready_count = ready_count;
best_future_count = future_count;
}
}
return best_extruder;
};
std::vector<LocalZWipeTowerToolchange> toolchanges;
int active_extruder = start_extruder;
size_t completed_passes = 0;
while (completed_passes < pass_states.size()) {
const int chosen_extruder = choose_ready_extruder(active_extruder);
if (chosen_extruder < 0) {
BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler deadlocked, falling back"
<< " start_extruder=" << start_extruder
<< " pass_count=" << pass_refs.size();
return collect_toolchanges_legacy(start_extruder);
}
if (active_extruder >= 0 && active_extruder != chosen_extruder)
toolchanges.push_back(LocalZWipeTowerToolchange{unsigned(active_extruder), unsigned(chosen_extruder)});
active_extruder = chosen_extruder;
bool completed_any = false;
std::vector<size_t> newly_completed;
for (size_t pass_state_idx = 0; pass_state_idx < pass_states.size(); ++pass_state_idx) {
PassState &pass_state = pass_states[pass_state_idx];
if (!pass_state.ready || pass_state.completed)
continue;
auto it_extruder = std::find(pass_state.remaining_extruders.begin(),
pass_state.remaining_extruders.end(),
unsigned(chosen_extruder));
if (it_extruder == pass_state.remaining_extruders.end())
continue;
pass_state.remaining_extruders.erase(it_extruder);
completed_any = true;
if (pass_state.remaining_extruders.empty())
newly_completed.push_back(pass_state_idx);
}
if (!completed_any) {
BOOST_LOG_TRIVIAL(warning) << "Local-Z wipe tower dependency scheduler made no progress, falling back"
<< " start_extruder=" << start_extruder
<< " active_extruder=" << active_extruder
<< " chosen_extruder=" << chosen_extruder
<< " pass_count=" << pass_refs.size();
return collect_toolchanges_legacy(start_extruder);
}
for (size_t pass_state_idx : newly_completed) {
PassState &pass_state = pass_states[pass_state_idx];
if (pass_state.completed)
continue;
pass_state.ready = false;
pass_state.completed = true;
++completed_passes;
const std::vector<size_t> &chain = chains[pass_state.chain_idx];
const size_t next_chain_pos = pass_state.chain_pos + 1;
if (next_chain_pos < chain.size())
pass_states[chain[next_chain_pos]].ready = true;
}
}
BOOST_LOG_TRIVIAL(info) << "Local-Z wipe tower dependency scheduler"
<< " start_extruder=" << start_extruder
<< " pass_count=" << pass_refs.size()
<< " chain_count=" << chains.size()
<< " toolchanges=" << toolchanges.size();
return toolchanges;
}
} // namespace
//BBS
// ORCA: Now this is a parameter
//float Print::min_skirt_length = 0;
@@ -275,6 +760,23 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "filament_shrinkage_compensation_z"
|| opt_key == "resolution"
|| opt_key == "precise_z_height"
// Mixed-filament and dithering keys require full reslicing so virtual
// filament assignments are re-evaluated and layer z-plans recomputed.
|| opt_key == "dithering_z_step_size"
|| opt_key == "dithering_local_z_mode"
|| opt_key == "dithering_local_z_whole_objects"
|| opt_key == "dithering_local_z_direct_multicolor"
|| opt_key == "dithering_step_painted_zones_only"
|| opt_key == "mixed_filament_gradient_mode"
|| opt_key == "mixed_color_layer_height_a"
|| opt_key == "mixed_color_layer_height_b"
|| 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"
// Spiral Vase forces different kind of slicing than the normal model:
// In Spiral Vase mode, holes are closed and only the largest area contour is kept at each layer.
// Therefore toggling the Spiral Vase on / off requires complete reslicing.
@@ -1358,7 +1860,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
layer_height_profiles.assign(m_objects.size(), std::vector<coordf_t>());
std::vector<coordf_t> &profile = layer_height_profiles[print_object_idx];
if (profile.empty())
PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile);
PrintObject::update_layer_height_profile(*print_object.model_object(), print_object.slicing_parameters(), profile, &print_object);
return profile;
};
@@ -2452,11 +2954,26 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::vector<std::vector<unsigned int>> all_filaments;
for (print_object_instance_sequential_active = print_object_instances_ordering.begin(); print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
const auto& mgr = this->mixed_filament_manager();
size_t num_phys = this->config().filament_colour.values.size();
for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) {
auto& layer_filament = tool_ordering.layer_tools()[idx].extruders;
all_filaments.emplace_back(layer_filament);
auto layer_filament = tool_ordering.layer_tools()[idx].extruders;
std::vector<unsigned int> expanded;
for (unsigned int u : layer_filament) {
if (mgr.is_mixed(u, num_phys)) {
if (auto* mf = mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(mf->component_a);
expanded.push_back(mf->component_b);
}
} else {
expanded.push_back(u);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
if (idx == 0)
first_layer_used_filaments.insert(first_layer_used_filaments.end(), layer_filament.begin(), layer_filament.end());
first_layer_used_filaments.insert(first_layer_used_filaments.end(), expanded.begin(), expanded.end());
all_filaments.emplace_back(std::move(expanded));
}
}
sort_remove_duplicates(first_layer_used_filaments);
@@ -3419,6 +3936,8 @@ void Print::_make_wipe_tower()
// Initialize the wipe tower.
WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes,
m_wipe_tower_data.tool_ordering.first_extruder());
const std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> layers_to_print = GCode::collect_layers_to_print(*this);
size_t layers_to_print_idx = 0;
// wipe_tower.set_retract();
// wipe_tower.set_zhop();
@@ -3437,10 +3956,52 @@ void Print::_make_wipe_tower()
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
if (!layer_tools.has_wipe_tower)
continue;
while (layers_to_print_idx + 1 < layers_to_print.size() &&
layers_to_print[layers_to_print_idx].first + EPSILON < layer_tools.print_z) {
++layers_to_print_idx;
}
const std::vector<GCode::LayerToPrint> *layers_with_same_print_z = nullptr;
if (layers_to_print_idx < layers_to_print.size() &&
std::abs(layers_to_print[layers_to_print_idx].first - layer_tools.print_z) <= EPSILON) {
layers_with_same_print_z = &layers_to_print[layers_to_print_idx].second;
}
bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front();
if (m_config.dithering_local_z_mode && layers_with_same_print_z != nullptr) {
const std::vector<LocalZWipeTowerToolchange> local_z_toolchanges =
collect_local_z_wipe_tower_toolchanges(*this, *layers_with_same_print_z, int(current_extruder_id));
if (!local_z_toolchanges.empty()) {
std::ostringstream local_z_sequence;
for (size_t toolchange_idx = 0; toolchange_idx < local_z_toolchanges.size(); ++toolchange_idx) {
if (toolchange_idx != 0)
local_z_sequence << ",";
local_z_sequence << local_z_toolchanges[toolchange_idx].old_tool << "->"
<< local_z_toolchanges[toolchange_idx].new_tool;
}
BOOST_LOG_TRIVIAL(debug) << "Local-Z wipe tower preplan"
<< " print_z=" << layer_tools.print_z
<< " start_tool=" << current_extruder_id
<< " nominal_toolchanges=" << layer_tools.extruders.size()
<< " local_z_toolchanges=" << local_z_toolchanges.size()
<< " sequence=" << local_z_sequence.str();
}
for (const LocalZWipeTowerToolchange &toolchange : local_z_toolchanges) {
wipe_tower.plan_local_z_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height,
toolchange.old_tool, toolchange.new_tool, (float) m_config.prime_volume);
}
if (!local_z_toolchanges.empty())
current_extruder_id = local_z_toolchanges.back().new_tool;
}
const std::vector<unsigned int> nominal_layer_extruders =
rotate_extruders_to_start_with(layer_tools.extruders, current_extruder_id);
wipe_tower.plan_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, current_extruder_id,
current_extruder_id, false);
for (const auto extruder_id : layer_tools.extruders) {
for (const auto extruder_id : nominal_layer_extruders) {
if ((first_layer && extruder_id == m_wipe_tower_data.tool_ordering.all_extruders().back()) || extruder_id !=
current_extruder_id) {
float volume_to_wipe = m_config.prime_volume;
@@ -3465,6 +4026,18 @@ void Print::_make_wipe_tower()
}
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
// Reserve Local-Z wipe-tower slots for unplanned toolchanges during Local-Z sub-layer emission.
if (m_config.dithering_local_z_mode) {
size_t local_z_reserve_slots = 0;
for (const PrintObject* print_object : m_objects)
local_z_reserve_slots += estimate_local_z_wipe_tower_reserve_slots(*print_object, layer_tools.print_z);
if (local_z_reserve_slots > 0) {
wipe_tower.plan_local_z_reserve((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height,
local_z_reserve_slots, (float) m_config.prime_volume);
}
}
if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0)
break;
}
@@ -3472,13 +4045,18 @@ void Print::_make_wipe_tower()
// Generate the wipe tower layers.
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
wipe_tower.generate(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.local_z_tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
wipe_tower.generate(m_wipe_tower_data.tool_changes, m_wipe_tower_data.local_z_tool_changes);
BOOST_LOG_TRIVIAL(debug) << "Wipe tower generation completed"
<< " nominal_layers=" << m_wipe_tower_data.tool_changes.size()
<< " local_z_layers=" << m_wipe_tower_data.local_z_tool_changes.size();
m_wipe_tower_data.depth = wipe_tower.get_depth();
m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs();
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height();
m_wipe_tower_data.bbx = wipe_tower.get_bbx();
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset();
m_wipe_tower_data.local_z_reserve_boxes = wipe_tower.get_local_z_reserve_boxes();
// Unload the current filament over the purge tower.
coordf_t layer_height = m_objects.front()->config().layer_height.value;
+57 -1
View File
@@ -4,6 +4,7 @@
#include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp"
#include "MixedFilament.hpp"
#include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp"
@@ -54,6 +55,35 @@ struct groupedVolumeSlices
ExPolygons slices;
};
// Phase A local-Z dithering planner cache.
struct LocalZInterval
{
size_t layer_id { 0 };
double z_lo { 0.0 };
double z_hi { 0.0 };
double base_height { 0.0 };
double sublayer_height { 0.0 };
bool has_mixed_paint { false };
size_t first_sublayer_idx { 0 };
size_t sublayer_count { 0 };
};
struct SubLayerPlan
{
size_t layer_id { 0 };
size_t pass_index { 0 };
bool split_interval { false };
double z_lo { 0.0 };
double z_hi { 0.0 };
double print_z { 0.0 };
double flow_height { 0.0 };
size_t dependency_group { 0 };
size_t dependency_order { 0 };
std::vector<ExPolygons> painted_masks_by_extruder;
std::vector<ExPolygons> fixed_painted_masks_by_extruder;
ExPolygons base_masks;
};
enum SupportNecessaryType {
NoNeedSupp=0,
SharpTail,
@@ -400,6 +430,19 @@ public:
std::shared_ptr<TreeSupportData> alloc_tree_support_preview_cache();
void clear_tree_support_preview_cache() { m_tree_support_preview_cache.reset(); }
const std::vector<LocalZInterval>& local_z_intervals() const { return m_local_z_intervals; }
const std::vector<SubLayerPlan>& local_z_sublayer_plan() const { return m_local_z_sublayer_plan; }
void set_local_z_plan(std::vector<LocalZInterval> intervals, std::vector<SubLayerPlan> sublayers)
{
m_local_z_intervals = std::move(intervals);
m_local_z_sublayer_plan = std::move(sublayers);
}
void clear_local_z_plan()
{
m_local_z_intervals.clear();
m_local_z_sublayer_plan.clear();
}
size_t support_layer_count() const { return m_support_layers.size(); }
void clear_support_layers();
SupportLayer* get_support_layer(int idx) { return idx<m_support_layers.size()? m_support_layers[idx]:nullptr; }
@@ -410,7 +453,10 @@ public:
// Initialize the layer_height_profile from the model_object's layer_height_profile, from model_object's layer height table, or from slicing parameters.
// Returns true, if the layer_height_profile was changed.
static bool update_layer_height_profile(const ModelObject &model_object, const SlicingParameters &slicing_parameters, std::vector<coordf_t> &layer_height_profile);
static bool update_layer_height_profile(const ModelObject &model_object,
const SlicingParameters &slicing_parameters,
std::vector<coordf_t> &layer_height_profile,
const PrintObject *print_object = nullptr);
// Collect the slicing parameters, to be used by variable layer thickness algorithm,
// by the interactive layer height editor and by the printing process itself.
@@ -557,6 +603,8 @@ private:
SlicingParameters m_slicing_params;
LayerPtrs m_layers;
SupportLayerPtrs m_support_layers;
std::vector<LocalZInterval> m_local_z_intervals;
std::vector<SubLayerPlan> m_local_z_sublayer_plan;
// BBS
std::shared_ptr<TreeSupportData> m_tree_support_preview_cache;
@@ -752,6 +800,7 @@ struct WipeTowerData
// Cache of tool changes per print layer.
std::unique_ptr<std::vector<WipeTower::ToolChangeResult>> priming;
std::vector<std::vector<WipeTower::ToolChangeResult>> tool_changes;
std::vector<std::vector<WipeTower::ToolChangeResult>> local_z_tool_changes;
std::unique_ptr<WipeTower::ToolChangeResult> final_purge;
std::vector<float> used_filament;
int number_of_toolchanges;
@@ -764,9 +813,12 @@ struct WipeTowerData
BoundingBoxf bbx;//including brim
Vec2f rib_offset;
std::optional<WipeTowerMeshData> wipe_tower_mesh_data;//added rib_offset
// Per-layer boxes reserved for Local-Z unplanned toolchanges.
std::vector<std::vector<WipeTower::box_coordinates>> local_z_reserve_boxes;
void clear() {
priming.reset(nullptr);
tool_changes.clear();
local_z_tool_changes.clear();
final_purge.reset(nullptr);
used_filament.clear();
number_of_toolchanges = -1;
@@ -774,6 +826,7 @@ struct WipeTowerData
brim_width = 0.f;
rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt;
local_z_reserve_boxes.clear();
}
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall);
@@ -946,6 +999,8 @@ public:
void auto_assign_extruders(ModelObject* model_object) const;
const PrintConfig& config() const { return m_config; }
const MixedFilamentManager& mixed_filament_manager() const { return m_mixed_filament_mgr; }
MixedFilamentManager& mixed_filament_manager() { return m_mixed_filament_mgr; }
const PrintObjectConfig& default_object_config() const { return m_default_object_config; }
const PrintRegionConfig& default_region_config() const { return m_default_region_config; }
ConstPrintObjectPtrsAdaptor objects() const { return ConstPrintObjectPtrsAdaptor(&m_objects); }
@@ -1134,6 +1189,7 @@ private:
Polygons first_layer_islands() const;
PrintConfig m_config;
MixedFilamentManager m_mixed_filament_mgr;
PrintObjectConfig m_default_object_config;
PrintRegionConfig m_default_region_config;
PrintObjectPtrs m_objects;
+186 -7
View File
@@ -1104,6 +1104,57 @@ static PrintObjectRegions* generate_print_object_regions(
return out.release();
}
// ---- Mixed-filament helpers used in Print::apply ----------------------------------------
static inline void append_unique_painted_extruder(std::vector<unsigned int> &painting_extruders,
unsigned int extruder_id,
size_t num_physical_extruders)
{
if (extruder_id < 1 || extruder_id > num_physical_extruders)
return;
if (std::find(painting_extruders.begin(), painting_extruders.end(), extruder_id) == painting_extruders.end())
painting_extruders.emplace_back(extruder_id);
}
// For a virtual (mixed) ID, expand to all physical component IDs it may resolve to.
// This pre-creates regions for every physical tool the mixed row can use so that
// apply_mm_segmentation can collapse mixed channels onto the correct region.
static void append_mixed_component_extruders(const MixedFilamentManager &mixed_mgr,
unsigned int state_id,
size_t num_physical_extruders,
std::vector<unsigned int> &painting_extruders)
{
if (state_id <= num_physical_extruders)
return;
const MixedFilament *mixed_row = mixed_mgr.mixed_filament_from_id(state_id, num_physical_extruders);
if (mixed_row == nullptr || !mixed_row->enabled)
return;
append_unique_painted_extruder(painting_extruders, mixed_row->component_a, num_physical_extruders);
append_unique_painted_extruder(painting_extruders, mixed_row->component_b, num_physical_extruders);
for (char token : mixed_row->gradient_component_ids) {
if (token < '1' || token > '9')
continue;
append_unique_painted_extruder(painting_extruders, unsigned(token - '0'), num_physical_extruders);
}
for (char token : mixed_row->manual_pattern) {
unsigned int extruder_id = 0;
if (token == '1')
extruder_id = mixed_row->component_a;
else if (token == '2')
extruder_id = mixed_row->component_b;
else if (token >= '3' && token <= '9')
extruder_id = unsigned(token - '0');
append_unique_painted_extruder(painting_extruders, extruder_id, num_physical_extruders);
}
}
// -----------------------------------------------------------------------------------------
Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config, bool extruder_applied)
{
#ifdef _DEBUG
@@ -1116,6 +1167,54 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
new_full_config.option("print_settings_id", true);
new_full_config.option("filament_settings_id", true);
new_full_config.option("printer_settings_id", true);
// Ensure mixed-filament and dithering keys are present so in-session updates are detected.
new_full_config.option("mixed_filament_gradient_mode", true);
new_full_config.option("mixed_color_layer_height_a", true);
new_full_config.option("mixed_color_layer_height_b", true);
new_full_config.option("mixed_filament_height_lower_bound", true);
new_full_config.option("mixed_filament_height_upper_bound", true);
new_full_config.option("mixed_filament_advanced_dithering", 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);
new_full_config.option("dithering_z_step_size", true);
new_full_config.option("dithering_local_z_mode", true);
new_full_config.option("dithering_local_z_whole_objects", true);
new_full_config.option("dithering_local_z_direct_multicolor", true);
new_full_config.option("dithering_step_painted_zones_only", true);
// Materialize the same keys on m_config so print_diff sees no phantom changes on cold start.
m_config.option("mixed_filament_gradient_mode", true);
m_config.option("mixed_color_layer_height_a", true);
m_config.option("mixed_color_layer_height_b", true);
m_config.option("mixed_filament_height_lower_bound", true);
m_config.option("mixed_filament_height_upper_bound", true);
m_config.option("mixed_filament_advanced_dithering", 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);
m_config.option("dithering_z_step_size", true);
m_config.option("dithering_local_z_mode", true);
m_config.option("dithering_local_z_whole_objects", true);
m_config.option("dithering_local_z_direct_multicolor", true);
m_config.option("dithering_step_painted_zones_only", true);
// Materialize the same keys on m_default_object_config for symmetry.
m_default_object_config.option("mixed_filament_gradient_mode", true);
m_default_object_config.option("mixed_color_layer_height_a", true);
m_default_object_config.option("mixed_color_layer_height_b", true);
m_default_object_config.option("mixed_filament_height_lower_bound", true);
m_default_object_config.option("mixed_filament_height_upper_bound", true);
m_default_object_config.option("mixed_filament_advanced_dithering", 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);
m_default_object_config.option("dithering_z_step_size", true);
m_default_object_config.option("dithering_local_z_mode", true);
m_default_object_config.option("dithering_local_z_whole_objects", true);
m_default_object_config.option("dithering_local_z_direct_multicolor", true);
m_default_object_config.option("dithering_step_painted_zones_only", true);
// BBS
std::vector <unsigned int> used_filaments = this->extruders(true);
@@ -1281,6 +1380,77 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
}
}
// Rebuild the mixed (virtual) filament manager from physical colours and user-defined
// custom entries. This must happen after m_config is up-to-date so filament_colour
// reflects the correct physical palette.
{
int mixed_gradient_mode = 0;
float mixed_height_lower = 0.04f;
float mixed_height_upper = 0.16f;
bool mixed_advanced_dither = false;
float mixed_surface_indentation = 0.f;
std::string mixed_custom_definitions;
if (new_full_config.has("mixed_filament_gradient_mode")) {
if (const ConfigOptionBool *opt = new_full_config.option<ConfigOptionBool>("mixed_filament_gradient_mode"))
mixed_gradient_mode = opt->value ? 1 : 0;
else
mixed_gradient_mode = new_full_config.opt_int("mixed_filament_gradient_mode");
}
if (new_full_config.has("mixed_filament_height_lower_bound"))
mixed_height_lower = float(new_full_config.opt_float("mixed_filament_height_lower_bound"));
if (new_full_config.has("mixed_filament_height_upper_bound"))
mixed_height_upper = float(new_full_config.opt_float("mixed_filament_height_upper_bound"));
if (new_full_config.has("mixed_filament_advanced_dithering")) {
if (const ConfigOptionBool *opt = new_full_config.option<ConfigOptionBool>("mixed_filament_advanced_dithering"))
mixed_advanced_dither = opt->value;
else
mixed_advanced_dither = (new_full_config.opt_int("mixed_filament_advanced_dithering") != 0);
}
if (new_full_config.has("mixed_filament_surface_indentation"))
mixed_surface_indentation = float(new_full_config.opt_float("mixed_filament_surface_indentation"));
if (new_full_config.has("mixed_filament_definitions"))
mixed_custom_definitions = new_full_config.opt_string("mixed_filament_definitions");
mixed_gradient_mode = std::clamp(mixed_gradient_mode, 0, 1);
mixed_height_lower = std::max(0.01f, mixed_height_lower);
mixed_height_upper = std::max(mixed_height_lower, mixed_height_upper);
mixed_surface_indentation = std::clamp(mixed_surface_indentation, -2.f, 2.f);
BOOST_LOG_TRIVIAL(info) << "Print::apply mixed settings"
<< ", gradient_mode=" << mixed_gradient_mode
<< ", lower=" << mixed_height_lower
<< ", upper=" << mixed_height_upper
<< ", advanced_dither=" << (mixed_advanced_dither ? 1 : 0)
<< ", surface_indentation=" << mixed_surface_indentation
<< ", custom_definitions_len=" << mixed_custom_definitions.size()
<< ", physical_extruders=" << num_extruders;
// Regenerate mixed (virtual) filaments from physical filament colours and
// re-apply user custom mixed definitions.
std::vector<std::string> physical_filament_colors = m_config.filament_colour.values;
physical_filament_colors.resize(num_extruders, "#26A69A");
m_mixed_filament_mgr.clear_custom_entries();
m_mixed_filament_mgr.auto_generate(physical_filament_colors);
m_mixed_filament_mgr.load_custom_entries(mixed_custom_definitions, physical_filament_colors);
m_mixed_filament_mgr.apply_gradient_settings(mixed_gradient_mode,
mixed_height_lower,
mixed_height_upper,
mixed_advanced_dither);
size_t mixed_custom_count = 0;
for (const auto &mf : m_mixed_filament_mgr.mixed_filaments())
if (mf.custom)
++mixed_custom_count;
BOOST_LOG_TRIVIAL(info) << "Print::apply mixed manager state"
<< ", mixed_total=" << m_mixed_filament_mgr.mixed_filaments().size()
<< ", mixed_enabled=" << m_mixed_filament_mgr.enabled_count()
<< ", mixed_custom=" << mixed_custom_count;
}
// Total filaments = physical extruders + enabled mixed (virtual) filaments.
// Used for extruder ID clamping so that virtual IDs are accepted.
const size_t num_total_filaments = m_mixed_filament_mgr.total_filaments(num_extruders);
ModelObjectStatusDB model_object_status_db;
// 1) Synchronize model objects.
@@ -1468,7 +1638,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
if (object_config_changed)
model_object.config.assign_config(model_object_new.config);
if (! object_diff.empty() || object_config_changed || num_extruders_changed ) {
PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders );
PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_total_filaments);
for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(model_object)) {
t_config_option_keys diff = print_object_status.print_object->config().diff(new_config);
if (! diff.empty()) {
@@ -1534,10 +1704,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Generate a list of trafos and XY offsets for instances of a ModelObject
// Producing the config for PrintObject on demand, caching it at print_object_last.
const PrintObject *print_object_last = nullptr;
auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders ](PrintObject *print_object) {
auto print_object_apply_config = [this, &print_object_last, model_object, num_total_filaments](PrintObject *print_object) {
print_object->config_apply(print_object_last ?
print_object_last->config() :
PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders ));
PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_total_filaments));
print_object_last = print_object;
};
if (old.empty()) {
@@ -1682,9 +1852,18 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
}
for (size_t state_idx = static_cast<size_t>(EnforcerBlockerType::Extruder1); state_idx < used_facet_states.size(); ++state_idx) {
if (used_facet_states[state_idx])
painting_extruders.emplace_back(state_idx);
if (!used_facet_states[state_idx])
continue;
if (state_idx <= num_total_filaments) {
painting_extruders.emplace_back(static_cast<unsigned int>(state_idx));
append_mixed_component_extruders(m_mixed_filament_mgr,
static_cast<unsigned int>(state_idx),
num_extruders,
painting_extruders);
}
}
std::sort(painting_extruders.begin(), painting_extruders.end());
painting_extruders.erase(std::unique(painting_extruders.begin(), painting_extruders.end()), painting_extruders.end());
}
if (model_object_status.print_object_regions_status == ModelObjectStatus::PrintObjectRegionsStatus::Valid) {
// Verify that the trafo for regions & volume bounding boxes thus for regions is still applicable.
@@ -1702,7 +1881,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
verify_update_print_object_regions(
print_object.model_object()->volumes,
m_default_region_config,
num_extruders,
num_total_filaments,
*print_object_regions,
[it_print_object, it_print_object_end, &update_apply_status](const PrintRegionConfig &old_config, const PrintRegionConfig &new_config, const t_config_option_keys &diff_keys) {
for (auto it = it_print_object; it != it_print_object_end; ++it)
@@ -1727,7 +1906,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
LayerRanges(print_object.model_object()->layer_config_ranges),
m_default_region_config,
model_object_status.print_instances.front().trafo,
num_extruders ,
num_total_filaments,
print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value),
painting_extruders,
print_object.is_fuzzy_skin_painted());
+223
View File
@@ -2404,6 +2404,177 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings{ "#F2754E" });
def = this->add("mixed_color_layer_height_a", coFloat);
def->label = L("Dithering cadence height A");
def->category = L("Others");
def->tooltip = L("Layer height contribution of component A for dithering virtual filaments. "
"Set to 0 to use normal 1-layer A / 1-layer B alternation.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_color_layer_height_b", coFloat);
def->label = L("Dithering cadence height B");
def->category = L("Others");
def->tooltip = L("Layer height contribution of component B for dithering virtual filaments. "
"Set to 0 to use normal 1-layer A / 1-layer B alternation.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_filament_gradient_mode", coBool);
def->label = L("Height-weighted cadence");
def->category = L("Others");
def->tooltip = L("Enable height-weighted cadence for mixed filaments. "
"Limitation: only one height-weighted mixed color should be present at a given Z plane, "
"because independent per-color layer heights are not supported and the resulting layer height applies to the whole plane. "
"When disabled, layer-cycle cadence is used.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("mixed_filament_height_lower_bound", coFloat);
def->label = L("Local-Z lower height bound");
def->category = L("Others");
def->tooltip = L("Lower bound used when Local-Z mixed-filament dithering chooses per-color sublayer heights.\n\n"
"Smaller values let Local-Z use thinner sublayers for a color when needed.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->sidetext = "mm";
def->min = 0.01;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.04));
def = this->add("mixed_filament_height_upper_bound", coFloat);
def->label = L("Local-Z upper height bound");
def->category = L("Others");
def->tooltip = L("Upper bound used when Local-Z mixed-filament dithering chooses per-color sublayer heights.\n\n"
"Larger values let Local-Z use thicker sublayers for a color when needed.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->sidetext = "mm";
def->min = 0.01;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.16));
def = this->add("mixed_filament_advanced_dithering", coBool);
def->label = L("Advanced dithering");
def->category = L("Others");
def->tooltip = L("Distribute mixed filament layer-cycle cadence using an advanced ordered dithering pattern "
"instead of a simple contiguous A-then-B run. This can reduce visible striping for some hues.\n\n"
"This is an even more experimental mode and the perceived color may differ from normal dithering "
"for the same filament pair and ratio.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
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");
def->tooltip = L("XY offset applied to mixed-filament painted regions before region assignment.\n\n"
"Positive values contract the mixed zone inward. Negative values expand it outward.\n\n"
"This applies to mixed filament usage in layer cadence, height cadence, same-layer pointillisme, and local Z dithering.");
def->sidetext = "mm";
def->min = -2.0;
def->max = 2.0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_filament_region_collapse", coBool);
def->label = L("Collapse same-color mixed regions");
def->category = L("Others");
def->tooltip = L("Merge ordinary mixed-filament painted regions into a single area when they resolve to the same physical filament on a layer.\n\n"
"This improves continuity for adjacent same-color areas. Local Z dithering turns this off automatically when enabled, but you may turn it back on manually.\n\n"
"Experimental with Local Z dithering.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("mixed_filament_definitions", coString);
def->label = L("Mixed filament custom definitions");
def->tooltip = L("Serialized custom mixed filament rows.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->gui_flags = "serialized";
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionString(""));
def = this->add("mixed_filament_pointillism_pixel_size", coFloat);
def->label = L("Pointillisme pixel size");
def->category = L("Others");
def->tooltip = L("Length of one pointillisme segment along an extrusion path for same-layer pointillisme mode. "
"Set to 0 to use automatic nozzle-based sizing.\n\n"
"Warning: Same-layer pointillisme is extremely experimental and may produce unusable results.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("mixed_filament_pointillism_line_gap", coFloat);
def->label = L("Pointillisme line gap");
def->category = L("Others");
def->tooltip = L("Optional non-extruded spacing between adjacent pointillisme segments. "
"Increase carefully to improve separation and print quality.\n\n"
"Warning: Same-layer pointillisme is extremely experimental and may produce unusable results.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("dithering_z_step_size", coFloat);
def->label = L("Dithering Z step size");
def->category = L("Others");
def->tooltip = L("Layer height used in Z zones painted with dithering (mixed virtual filaments). "
"Set to 0 to keep normal layer height in those zones.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->sidetext = "mm";
def->min = 0.;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("dithering_local_z_mode", coBool);
def->label = L("Local Z dithering mode");
def->category = L("Others");
def->tooltip = L("Use Variable Layers for Color Blending\n\n"
"Blend colors by varying layer heights instead of using a fixed ratio of equal-height layers. This only affects blended color zones; non-blended areas keep their nominal layer height and cadence when possible.\n\n"
"This setting increases color blending smoothness by splitting each blended layer according to the blend ratio. For example, a 66/33 blend at 0.12 mm layer height will print as one 0.08 mm layer and one 0.04 mm layer. At 0.20 mm layer height, a 75/25 blend will print as one 0.15 mm layer and one 0.05 mm layer.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("dithering_local_z_whole_objects", coBool);
def->label = L("Apply Local-Z to whole mixed objects");
def->category = L("Others");
def->tooltip = L("Experimental. Extend Local-Z dithering beyond painted mixed zones so mixed wall regions can use Local-Z across the whole object.\n\n"
"This also lets Local-Z continue through default mixed walls around painted areas instead of limiting the effect strictly to painted masks.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("dithering_local_z_direct_multicolor", coBool);
def->label = L("Use direct multicolor Local-Z solver");
def->category = L("Others");
def->tooltip = L("Experimental. For mixed rows with 3 or more physical filaments, allocate Local-Z sublayers directly across all components with carry-over error between layers instead of collapsing them into pair cadence.\n\n"
"This can reduce visible banding in multicolor Local-Z blends at the cost of more toolchanges. It is ignored when explicit Local-Z A/B heights are set.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("dithering_step_painted_zones_only", coBool);
def->label = L("Use step size in painted zones only");
def->category = L("Others");
def->tooltip = L("When enabled, dithering Z step size is applied only where mixed filament is painted. "
"Unpainted zones keep their original layer height.\n\n"
"Detailed mixed filament setting explanations will be published once the project wiki is available.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
// PS
def = this->add("filament_notes", coStrings);
def->label = L("Filament notes");
@@ -4077,6 +4248,31 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("enable_infill_filament_override", coBool);
def->label = L("Override infill filament");
def->category = L("Extruders");
def->tooltip = L("Allow this print, object, or part to use a dedicated filament for sparse infill instead of inheriting its regular filament.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("infill_filament_use_base_first_layers", coInt);
def->label = L("Base infill on first layers");
def->category = L("Extruders");
def->tooltip = L("Keep using the regular object filament for this many bottom infill layers before switching to the infill override filament.");
def->sidetext = L("layers");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("infill_filament_use_base_last_layers", coInt);
def->label = L("Base infill on last layers");
def->category = L("Extruders");
def->tooltip = L("Keep using the regular object filament for this many top infill layers after switching back from the infill override filament.");
def->sidetext = L("layers");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0));
def = this->add("sparse_infill_line_width", coFloatOrPercent);
def->label = L("Sparse infill");
def->category = L("Quality");
@@ -6959,6 +7155,16 @@ void PrintConfigDef::init_fff_params()
def->max = 300.;
def->set_default_value(new ConfigOptionPercent(100.));
def = this->add("local_z_wipe_tower_purge_lines", coFloat);
def->label = L("Local-Z mini wipe lines");
def->tooltip = L("Number of purge lines reserved for each runtime Local-Z wipe tower toolchange. "
"Higher values improve cleanup but increase tower depth. "
"Only used when Local-Z dithering and the prime tower are enabled.");
def->sidetext = L("lines");
def->mode = comAdvanced;
def->min = 1.0;
def->set_default_value(new ConfigOptionFloat(3.0));
def = this->add("idle_temperature", coInts);
def->label = L("Idle temperature");
def->tooltip = L("Nozzle temperature when the tool is currently not used in multi-tool setups. "
@@ -8571,6 +8777,23 @@ t_config_option_keys DynamicPrintConfig::normalize_fdm_2(int num_objects, int us
*/
}
// Mixed-filament lifecycle anchor.
// Side-effect: PresetBundle observes the mixed_filament_definitions string change and rebuilds the manager
// (see PresetBundle::sync_mixed_filaments_from_config). Nothing to do here —
// keep this comment as a lifecycle anchor for future maintainers.
if (auto* defs = this->option<ConfigOptionString>("mixed_filament_definitions")) {
(void)defs;
}
// Backward-compat: if an older project set sparse_infill_filament to a non-wall value
// but did not set enable_infill_filament_override, infer the flag.
if (!this->has("enable_infill_filament_override") && this->has("sparse_infill_filament")) {
int wall = this->has("wall_filament") ? this->opt_int("wall_filament") : 1;
int sparse = this->opt_int("sparse_infill_filament");
if (sparse > 0 && sparse != wall)
this->set_key_value("enable_infill_filament_override", new ConfigOptionBool(true));
}
return changed_keys;
}
+21
View File
@@ -1121,6 +1121,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercent, fuzzy_skin_ripple_offset))
((ConfigOptionInt, fuzzy_skin_layers_between_ripple_offset))
((ConfigOptionFloat, gap_infill_speed))
((ConfigOptionBool, enable_infill_filament_override))
((ConfigOptionInt, infill_filament_use_base_first_layers))
((ConfigOptionInt, infill_filament_use_base_last_layers))
((ConfigOptionInt, sparse_infill_filament))
((ConfigOptionFloatOrPercent, sparse_infill_line_width))
((ConfigOptionPercent, infill_wall_overlap))
@@ -1551,6 +1554,23 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, ooze_prevention))
((ConfigOptionString, filename_format))
((ConfigOptionStrings, post_process))
((ConfigOptionFloat, mixed_color_layer_height_a))
((ConfigOptionFloat, mixed_color_layer_height_b))
((ConfigOptionBool, mixed_filament_gradient_mode))
((ConfigOptionFloat, mixed_filament_height_lower_bound))
((ConfigOptionFloat, mixed_filament_height_upper_bound))
((ConfigOptionBool, mixed_filament_advanced_dithering))
((ConfigOptionBool, mixed_filament_component_bias_enabled))
((ConfigOptionFloat, mixed_filament_surface_indentation))
((ConfigOptionBool, mixed_filament_region_collapse))
((ConfigOptionString, mixed_filament_definitions))
((ConfigOptionFloat, mixed_filament_pointillism_pixel_size))
((ConfigOptionFloat, mixed_filament_pointillism_line_gap))
((ConfigOptionFloat, dithering_z_step_size))
((ConfigOptionBool, dithering_local_z_mode))
((ConfigOptionBool, dithering_local_z_whole_objects))
((ConfigOptionBool, dithering_local_z_direct_multicolor))
((ConfigOptionBool, dithering_step_painted_zones_only))
((ConfigOptionString, printer_model))
((ConfigOptionFloat, resolution))
((ConfigOptionFloats, retraction_minimum_travel))
@@ -1593,6 +1613,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, enable_tower_interface_cooldown_during_tower))
((ConfigOptionFloat, wipe_tower_bridging))
((ConfigOptionPercent, wipe_tower_extra_flow))
((ConfigOptionFloat, local_z_wipe_tower_purge_lines))
((ConfigOptionFloats, flush_volumes_matrix))
((ConfigOptionFloats, flush_volumes_vector))
+574 -8
View File
@@ -1138,6 +1138,13 @@ bool PrintObject::invalidate_state_by_config_options(
steps.emplace_back(posPerimeters);
} else if (
opt_key == "layer_height"
|| opt_key == "dithering_z_step_size"
|| opt_key == "dithering_local_z_mode"
|| opt_key == "dithering_local_z_whole_objects"
|| opt_key == "dithering_local_z_direct_multicolor"
|| 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"
|| opt_key == "raft_layers"
@@ -1154,6 +1161,19 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "interlocking_depth"
|| opt_key == "interlocking_boundary_avoidance"
|| opt_key == "interlocking_beam_width") {
steps.emplace_back(posSlice);
} else if (
opt_key == "mixed_filament_gradient_mode"
|| 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") {
// Mixed filament gradient controls affect layer cadence and virtual
// tool distribution, so force a re-slice prompt like other
// layer-structure settings.
steps.emplace_back(posSlice);
} else if (
opt_key == "elefant_foot_compensation"
@@ -3720,16 +3740,564 @@ std::vector<unsigned int> PrintObject::object_extruders() const
return extruders;
}
bool PrintObject::update_layer_height_profile(const ModelObject &model_object, const SlicingParameters &slicing_parameters, std::vector<coordf_t> &layer_height_profile)
namespace {
struct LayerHeightRangeOverride {
coordf_t lo { 0.f };
coordf_t hi { 0.f };
coordf_t height { 0.f };
};
struct MixedStateZRanges {
size_t state_id { 0 };
std::vector<t_layer_height_range> ranges;
};
struct MixedStateCadence {
size_t state_id { 0 };
coordf_t height_a { 0.f };
coordf_t height_b { 0.f };
std::vector<t_layer_height_range> ranges;
};
static void sort_and_merge_layer_ranges(std::vector<t_layer_height_range> &ranges)
{
if (ranges.empty())
return;
std::sort(ranges.begin(), ranges.end(), [](const t_layer_height_range &a, const t_layer_height_range &b) {
return a.first < b.first || (a.first == b.first && a.second < b.second);
});
std::vector<t_layer_height_range> merged;
merged.reserve(ranges.size());
for (const t_layer_height_range &range : ranges) {
if (range.second <= range.first + EPSILON)
continue;
if (merged.empty() || range.first > merged.back().second + EPSILON) {
merged.emplace_back(range);
} else {
merged.back().second = std::max(merged.back().second, range.second);
}
}
ranges = std::move(merged);
}
static std::vector<t_layer_height_range> collect_mixed_painted_z_ranges(const PrintObject &print_object, coordf_t object_height)
{
std::vector<t_layer_height_range> mixed_ranges;
const Print *print = print_object.print();
if (object_height <= EPSILON || print == nullptr)
return mixed_ranges;
const size_t num_physical = print->config().filament_colour.size();
const size_t num_total = print->mixed_filament_manager().total_filaments(num_physical);
if (num_total <= num_physical)
return mixed_ranges;
const size_t max_state = std::min(num_total, size_t(EnforcerBlockerType::ExtruderMax));
std::vector<std::vector<t_layer_height_range>> per_state(max_state + 1);
const Transform3d object_to_print = print_object.trafo_centered();
for (const ModelVolume *mv : print_object.model_object()->volumes) {
if (mv == nullptr || !mv->is_model_part() || mv->mmu_segmentation_facets.empty())
continue;
const auto &used_states = mv->mmu_segmentation_facets.get_data().used_states;
if (used_states.empty())
continue;
const Transform3d volume_to_print = object_to_print * mv->get_matrix();
constexpr coordf_t thin_band = 0.01f;
for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) {
if (state_idx >= used_states.size() || !used_states[state_idx])
continue;
const auto facets = mv->mmu_segmentation_facets.get_facets_strict(*mv, static_cast<EnforcerBlockerType>(state_idx));
if (facets.indices.empty() || facets.vertices.empty())
continue;
auto &state_ranges = per_state[state_idx];
for (const auto &face : facets.indices) {
double tri_z_min = DBL_MAX;
double tri_z_max = -DBL_MAX;
for (int i = 0; i < 3; ++i) {
const size_t vertex_idx = size_t(face[i]);
if (vertex_idx >= facets.vertices.size())
continue;
const Vec3d p = volume_to_print * facets.vertices[vertex_idx].cast<double>();
tri_z_min = std::min(tri_z_min, p.z());
tri_z_max = std::max(tri_z_max, p.z());
}
if (tri_z_min == DBL_MAX || tri_z_max == -DBL_MAX)
continue;
coordf_t lo = std::max<coordf_t>(0.f, coordf_t(tri_z_min));
coordf_t hi = std::min<coordf_t>(object_height, coordf_t(tri_z_max));
if (hi <= lo + EPSILON) {
const coordf_t center = std::max<coordf_t>(0.f, std::min<coordf_t>(object_height, lo));
lo = std::max<coordf_t>(0.f, center - thin_band * 0.5f);
hi = std::min<coordf_t>(object_height, center + thin_band * 0.5f);
}
if (lo + EPSILON < hi)
state_ranges.emplace_back(lo, hi);
}
}
}
for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) {
auto &state_ranges = per_state[state_idx];
if (state_ranges.empty())
continue;
sort_and_merge_layer_ranges(state_ranges);
mixed_ranges.insert(mixed_ranges.end(), state_ranges.begin(), state_ranges.end());
}
sort_and_merge_layer_ranges(mixed_ranges);
return mixed_ranges;
}
static std::vector<MixedStateZRanges> collect_mixed_painted_z_ranges_by_state(const PrintObject &print_object, coordf_t object_height)
{
std::vector<MixedStateZRanges> out;
const Print *print = print_object.print();
if (object_height <= EPSILON || print == nullptr)
return out;
const size_t num_physical = print->config().filament_colour.size();
const size_t num_total = print->mixed_filament_manager().total_filaments(num_physical);
if (num_total <= num_physical)
return out;
const size_t max_state = std::min(num_total, size_t(EnforcerBlockerType::ExtruderMax));
std::vector<std::vector<t_layer_height_range>> per_state(max_state + 1);
const Transform3d object_to_print = print_object.trafo_centered();
for (const ModelVolume *mv : print_object.model_object()->volumes) {
if (mv == nullptr || !mv->is_model_part() || mv->mmu_segmentation_facets.empty())
continue;
const auto &used_states = mv->mmu_segmentation_facets.get_data().used_states;
if (used_states.empty())
continue;
const Transform3d volume_to_print = object_to_print * mv->get_matrix();
constexpr coordf_t thin_band = 0.01f;
for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) {
if (state_idx >= used_states.size() || !used_states[state_idx])
continue;
const auto facets = mv->mmu_segmentation_facets.get_facets_strict(*mv, static_cast<EnforcerBlockerType>(state_idx));
if (facets.indices.empty() || facets.vertices.empty())
continue;
auto &state_ranges = per_state[state_idx];
for (const auto &face : facets.indices) {
double tri_z_min = DBL_MAX;
double tri_z_max = -DBL_MAX;
for (int i = 0; i < 3; ++i) {
const size_t vertex_idx = size_t(face[i]);
if (vertex_idx >= facets.vertices.size())
continue;
const Vec3d p = volume_to_print * facets.vertices[vertex_idx].cast<double>();
tri_z_min = std::min(tri_z_min, p.z());
tri_z_max = std::max(tri_z_max, p.z());
}
if (tri_z_min == DBL_MAX || tri_z_max == -DBL_MAX)
continue;
coordf_t lo = std::max<coordf_t>(0.f, coordf_t(tri_z_min));
coordf_t hi = std::min<coordf_t>(object_height, coordf_t(tri_z_max));
if (hi <= lo + EPSILON) {
const coordf_t center = std::max<coordf_t>(0.f, std::min<coordf_t>(object_height, lo));
lo = std::max<coordf_t>(0.f, center - thin_band * 0.5f);
hi = std::min<coordf_t>(object_height, center + thin_band * 0.5f);
}
if (lo + EPSILON < hi)
state_ranges.emplace_back(lo, hi);
}
}
}
out.reserve(max_state > num_physical ? max_state - num_physical : 0);
for (size_t state_idx = num_physical + 1; state_idx <= max_state; ++state_idx) {
auto &state_ranges = per_state[state_idx];
if (state_ranges.empty())
continue;
sort_and_merge_layer_ranges(state_ranges);
if (!state_ranges.empty())
out.push_back({ state_idx, std::move(state_ranges) });
}
return out;
}
static std::vector<LayerHeightRangeOverride> base_layer_height_overrides(const t_layer_config_ranges &ranges, coordf_t object_height)
{
std::vector<LayerHeightRangeOverride> out;
out.reserve(ranges.size());
coordf_t last_hi = 0.f;
for (const auto &[range, config] : ranges) {
coordf_t lo = std::max(range.first, last_hi);
coordf_t hi = std::min(range.second, object_height);
if (lo + EPSILON >= hi)
continue;
const ConfigOption *layer_height_opt = config.option("layer_height");
if (layer_height_opt == nullptr)
continue;
out.push_back({ lo, hi, coordf_t(layer_height_opt->getFloat()) });
last_hi = hi;
}
return out;
}
static bool contains_z(const std::vector<t_layer_height_range> &ranges, coordf_t z)
{
for (const t_layer_height_range &range : ranges) {
if (z + EPSILON < range.first)
break;
if (z + EPSILON >= range.first && z < range.second - EPSILON)
return true;
}
return false;
}
static bool get_override_height(const std::vector<LayerHeightRangeOverride> &ranges, coordf_t z, coordf_t &height_out)
{
for (const LayerHeightRangeOverride &range : ranges) {
if (z + EPSILON < range.lo)
break;
if (z + EPSILON >= range.lo && z < range.hi - EPSILON) {
height_out = range.height;
return true;
}
}
return false;
}
static t_layer_config_ranges layer_ranges_with_dithering(const t_layer_config_ranges &base_ranges_map,
coordf_t object_height,
coordf_t default_layer_height,
const std::vector<t_layer_height_range> &mixed_ranges,
coordf_t dithering_step)
{
if (object_height <= EPSILON || mixed_ranges.empty() || dithering_step <= EPSILON)
return base_ranges_map;
const std::vector<LayerHeightRangeOverride> base_ranges = base_layer_height_overrides(base_ranges_map, object_height);
std::vector<coordf_t> boundaries;
boundaries.reserve(2 + base_ranges.size() * 2 + mixed_ranges.size() * 2);
boundaries.emplace_back(0.f);
boundaries.emplace_back(object_height);
for (const LayerHeightRangeOverride &range : base_ranges) {
boundaries.emplace_back(range.lo);
boundaries.emplace_back(range.hi);
}
for (const t_layer_height_range &range : mixed_ranges) {
boundaries.emplace_back(range.first);
boundaries.emplace_back(range.second);
}
std::sort(boundaries.begin(), boundaries.end());
boundaries.erase(std::unique(boundaries.begin(), boundaries.end(), [](coordf_t a, coordf_t b) {
return std::abs(a - b) <= EPSILON;
}),
boundaries.end());
std::vector<LayerHeightRangeOverride> merged_ranges;
merged_ranges.reserve(boundaries.size());
for (size_t i = 1; i < boundaries.size(); ++i) {
const coordf_t lo = boundaries[i - 1];
const coordf_t hi = boundaries[i];
if (hi <= lo + EPSILON)
continue;
const coordf_t z_mid = 0.5f * (lo + hi);
coordf_t target_height = default_layer_height;
coordf_t base_height = 0.f;
if (contains_z(mixed_ranges, z_mid)) {
target_height = dithering_step;
} else if (get_override_height(base_ranges, z_mid, base_height)) {
target_height = base_height;
}
if (std::abs(target_height - default_layer_height) <= EPSILON)
continue;
if (!merged_ranges.empty() &&
std::abs(merged_ranges.back().height - target_height) <= EPSILON &&
std::abs(merged_ranges.back().hi - lo) <= EPSILON) {
merged_ranges.back().hi = hi;
} else {
merged_ranges.push_back({ lo, hi, target_height });
}
}
t_layer_config_ranges out;
for (const LayerHeightRangeOverride &range : merged_ranges) {
if (range.hi <= range.lo + EPSILON)
continue;
ModelConfig cfg;
cfg.set_key_value("layer_height", new ConfigOptionFloat(range.height));
out.emplace(t_layer_height_range(range.lo, range.hi), std::move(cfg));
}
return out;
}
static bool mixed_state_heights(const MixedFilamentManager &mixed_mgr,
size_t num_physical,
size_t state_id,
coordf_t lower_bound,
coordf_t upper_bound,
coordf_t &height_a,
coordf_t &height_b)
{
if (state_id <= num_physical)
return false;
const size_t idx = state_id - num_physical - 1;
const auto &mixed = mixed_mgr.mixed_filaments();
if (idx >= mixed.size())
return false;
const int mix_b = std::clamp(mixed[idx].mix_b_percent, 0, 100);
const coordf_t pct_b = coordf_t(mix_b) / coordf_t(100.f);
const coordf_t pct_a = coordf_t(1.f) - pct_b;
const coordf_t lo = std::max<coordf_t>(0.01f, lower_bound);
const coordf_t hi = std::max<coordf_t>(lo, upper_bound);
height_a = std::max<coordf_t>(0.01f, lo + pct_a * (hi - lo));
height_b = std::max<coordf_t>(0.01f, lo + pct_b * (hi - lo));
return true;
}
static coordf_t mixed_state_height_at_z(const MixedStateCadence &state, coordf_t z)
{
const coordf_t cycle = std::max<coordf_t>(0.01f, state.height_a + state.height_b);
coordf_t phase = std::fmod(std::max<coordf_t>(0.f, z), cycle);
if (phase < 0.f)
phase += cycle;
return (phase < state.height_a) ? state.height_a : state.height_b;
}
static void append_state_cadence_boundaries(std::vector<coordf_t> &boundaries,
const t_layer_height_range &range,
coordf_t height_a,
coordf_t height_b)
{
const coordf_t cycle = height_a + height_b;
if (cycle <= EPSILON || range.second <= range.first + EPSILON)
return;
const int k_start = int(std::floor(range.first / cycle)) - 1;
coordf_t boundary = coordf_t(k_start) * cycle;
size_t guard = 0;
while (boundary <= range.second + cycle + EPSILON && guard++ < 200000) {
if (boundary > range.first + EPSILON && boundary < range.second - EPSILON)
boundaries.emplace_back(boundary);
const coordf_t split = boundary + height_a;
if (split > range.first + EPSILON && split < range.second - EPSILON)
boundaries.emplace_back(split);
boundary += cycle;
}
}
static t_layer_config_ranges layer_ranges_with_height_weighted_mixed(
const t_layer_config_ranges &base_ranges_map,
coordf_t object_height,
coordf_t default_layer_height,
const std::vector<MixedStateZRanges> &mixed_state_ranges,
const MixedFilamentManager &mixed_mgr,
size_t num_physical,
coordf_t lower_bound,
coordf_t upper_bound)
{
if (object_height <= EPSILON || mixed_state_ranges.empty())
return base_ranges_map;
const std::vector<LayerHeightRangeOverride> base_ranges = base_layer_height_overrides(base_ranges_map, object_height);
std::vector<MixedStateCadence> states;
states.reserve(mixed_state_ranges.size());
for (const MixedStateZRanges &state_ranges : mixed_state_ranges) {
coordf_t height_a = 0.f;
coordf_t height_b = 0.f;
if (!mixed_state_heights(mixed_mgr, num_physical, state_ranges.state_id, lower_bound, upper_bound, height_a, height_b))
continue;
states.push_back({ state_ranges.state_id, height_a, height_b, state_ranges.ranges });
}
if (states.empty())
return base_ranges_map;
std::vector<coordf_t> boundaries;
boundaries.reserve(2 + base_ranges.size() * 2 + states.size() * 64);
boundaries.emplace_back(0.f);
boundaries.emplace_back(object_height);
for (const LayerHeightRangeOverride &range : base_ranges) {
boundaries.emplace_back(range.lo);
boundaries.emplace_back(range.hi);
}
for (const MixedStateCadence &state : states) {
for (const t_layer_height_range &range : state.ranges) {
boundaries.emplace_back(range.first);
boundaries.emplace_back(range.second);
append_state_cadence_boundaries(boundaries, range, state.height_a, state.height_b);
}
}
std::sort(boundaries.begin(), boundaries.end());
boundaries.erase(std::unique(boundaries.begin(), boundaries.end(), [](coordf_t a, coordf_t b) {
return std::abs(a - b) <= EPSILON;
}),
boundaries.end());
std::vector<LayerHeightRangeOverride> merged_ranges;
merged_ranges.reserve(boundaries.size());
for (size_t i = 1; i < boundaries.size(); ++i) {
const coordf_t lo = boundaries[i - 1];
const coordf_t hi = boundaries[i];
if (hi <= lo + EPSILON)
continue;
const coordf_t z_mid = 0.5f * (lo + hi);
coordf_t target_height = default_layer_height;
coordf_t base_height = 0.f;
const bool has_base_override = get_override_height(base_ranges, z_mid, base_height);
if (has_base_override)
target_height = base_height;
bool has_mixed = false;
coordf_t mixed_height = target_height;
for (const MixedStateCadence &state : states) {
if (!contains_z(state.ranges, z_mid))
continue;
const coordf_t state_height = mixed_state_height_at_z(state, z_mid);
if (!has_mixed) {
mixed_height = state_height;
has_mixed = true;
} else {
mixed_height = std::min(mixed_height, state_height);
}
}
if (has_mixed)
target_height = mixed_height;
if (std::abs(target_height - default_layer_height) <= EPSILON)
continue;
if (!merged_ranges.empty() &&
std::abs(merged_ranges.back().height - target_height) <= EPSILON &&
std::abs(merged_ranges.back().hi - lo) <= EPSILON) {
merged_ranges.back().hi = hi;
} else {
merged_ranges.push_back({ lo, hi, target_height });
}
}
t_layer_config_ranges out;
for (const LayerHeightRangeOverride &range : merged_ranges) {
if (range.hi <= range.lo + EPSILON)
continue;
ModelConfig cfg;
cfg.set_key_value("layer_height", new ConfigOptionFloat(range.height));
out.emplace(t_layer_height_range(range.lo, range.hi), std::move(cfg));
}
return out;
}
} // namespace
bool PrintObject::update_layer_height_profile(const ModelObject &model_object,
const SlicingParameters &slicing_parameters,
std::vector<coordf_t> &layer_height_profile,
const PrintObject *print_object)
{
bool updated = false;
const t_layer_config_ranges *ranges_to_use = &model_object.layer_config_ranges;
t_layer_config_ranges mixed_gradient_ranges;
t_layer_config_ranges dithering_ranges;
if (print_object != nullptr && print_object->print() != nullptr) {
const DynamicPrintConfig &full_cfg = print_object->print()->full_print_config();
const PrintConfig &print_cfg = print_object->print()->config();
bool height_weighted_mode = print_cfg.mixed_filament_gradient_mode.value;
if (full_cfg.has("mixed_filament_gradient_mode")) {
if (const ConfigOptionBool *opt = full_cfg.option<ConfigOptionBool>("mixed_filament_gradient_mode"))
height_weighted_mode = opt->value;
else if (const ConfigOptionInt *opt = full_cfg.option<ConfigOptionInt>("mixed_filament_gradient_mode"))
height_weighted_mode = (opt->value != 0);
}
coordf_t mixed_lower = coordf_t(print_cfg.mixed_filament_height_lower_bound.value);
coordf_t mixed_upper = coordf_t(print_cfg.mixed_filament_height_upper_bound.value);
if (full_cfg.has("mixed_filament_height_lower_bound"))
mixed_lower = coordf_t(full_cfg.opt_float("mixed_filament_height_lower_bound"));
if (full_cfg.has("mixed_filament_height_upper_bound"))
mixed_upper = coordf_t(full_cfg.opt_float("mixed_filament_height_upper_bound"));
mixed_lower = std::max<coordf_t>(0.01f, mixed_lower);
mixed_upper = std::max<coordf_t>(mixed_lower, mixed_upper);
if (height_weighted_mode) {
const coordf_t object_height = slicing_parameters.object_print_z_uncompensated_height();
const auto mixed_states = collect_mixed_painted_z_ranges_by_state(*print_object, object_height);
if (!mixed_states.empty()) {
mixed_gradient_ranges = layer_ranges_with_height_weighted_mixed(*ranges_to_use,
object_height,
slicing_parameters.layer_height,
mixed_states,
print_object->print()->mixed_filament_manager(),
print_cfg.filament_colour.size(),
mixed_lower,
mixed_upper);
ranges_to_use = &mixed_gradient_ranges;
}
}
coordf_t dithering_step = coordf_t(print_object->print()->config().dithering_z_step_size.value);
bool local_z_mode = print_object->print()->config().dithering_local_z_mode.value;
bool painted_zones_only = print_object->print()->config().dithering_step_painted_zones_only.value;
if (full_cfg.has("dithering_z_step_size"))
dithering_step = coordf_t(full_cfg.opt_float("dithering_z_step_size"));
if (full_cfg.has("dithering_local_z_mode")) {
if (const ConfigOptionBool *opt = full_cfg.option<ConfigOptionBool>("dithering_local_z_mode"))
local_z_mode = opt->value;
else if (const ConfigOptionInt *opt = full_cfg.option<ConfigOptionInt>("dithering_local_z_mode"))
local_z_mode = (opt->value != 0);
}
if (full_cfg.has("dithering_step_painted_zones_only"))
painted_zones_only = full_cfg.opt_bool("dithering_step_painted_zones_only");
if (!height_weighted_mode && !local_z_mode && dithering_step > EPSILON) {
const coordf_t object_height = slicing_parameters.object_print_z_uncompensated_height();
std::vector<t_layer_height_range> mixed_ranges;
if (painted_zones_only)
mixed_ranges = collect_mixed_painted_z_ranges(*print_object, object_height);
else if (object_height > EPSILON)
mixed_ranges.emplace_back(0.f, object_height);
if (!mixed_ranges.empty()) {
dithering_ranges = layer_ranges_with_dithering(*ranges_to_use,
object_height,
slicing_parameters.layer_height,
mixed_ranges,
dithering_step);
ranges_to_use = &dithering_ranges;
}
}
}
const bool has_dithering_ranges = (ranges_to_use != &model_object.layer_config_ranges);
if (layer_height_profile.empty()) {
// use the constructor because the assignement is crashing on ASAN OsX
layer_height_profile = std::vector<coordf_t>(model_object.layer_height_profile.get());
// layer_height_profile = model_object.layer_height_profile;
// The layer height returned is sampled with high density for the UI layer height painting
// and smoothing tool to work.
updated = true;
}
@@ -3741,10 +4309,8 @@ bool PrintObject::update_layer_height_profile(const ModelObject &model_object, c
std::abs(layer_height_profile[layer_height_profile.size() - 2] - slicing_parameters.object_print_z_uncompensated_max + slicing_parameters.object_print_z_min) > 1e-3))
layer_height_profile.clear();
if (layer_height_profile.empty() || layer_height_profile[1] != slicing_parameters.first_object_layer_height) {
//layer_height_profile = layer_height_profile_adaptive(slicing_parameters, model_object.layer_config_ranges, model_object.volumes);
layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, model_object.layer_config_ranges);
// The layer height profile is already compressed.
if (layer_height_profile.empty() || layer_height_profile[1] != slicing_parameters.first_object_layer_height || has_dithering_ranges) {
layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, *ranges_to_use);
updated = true;
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -2524,4 +2524,17 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting(
return target_selector.serialize();
}
void TriangleSelector::shift_states_above(EnforcerBlockerType threshold, int delta)
{
for (Triangle &triangle : m_triangles) {
if (triangle.is_split() || !triangle.valid()) continue;
EnforcerBlockerType s = triangle.get_state();
if (s != EnforcerBlockerType::NONE && s >= threshold) {
int new_val = static_cast<int>(s) + delta;
if (new_val >= 0)
triangle.set_state(EnforcerBlockerType(new_val));
}
}
}
} // namespace Slic3r
+3
View File
@@ -391,6 +391,9 @@ public:
const indexed_triangle_set& target_its,
const Transform3d& target_transform,
const std::optional<std::reference_wrapper<const TriangleSplittingData>>& existing_painting);
// Shift all non-NONE leaf triangle states >= threshold by delta.
// Used to renumber painted filament IDs after a filament slot is inserted or removed.
void shift_states_above(EnforcerBlockerType threshold, int delta);
protected:
// Triangle and info about how it's split.
+81
View File
@@ -0,0 +1,81 @@
#include "filament_mixer.h"
#include <algorithm>
#include <cmath>
#include "filament_mixer_model.h"
namespace Slic3r {
namespace {
inline float clamp01(float x)
{
return std::max(0.0f, std::min(1.0f, x));
}
inline float srgb_to_linear(float x)
{
return (x >= 0.04045f) ? std::pow((x + 0.055f) / 1.055f, 2.4f) : x / 12.92f;
}
inline float linear_to_srgb(float x)
{
return (x >= 0.0031308f) ? (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f) : (12.92f * x);
}
inline unsigned char to_u8(float x)
{
const float clamped = clamp01(x);
return static_cast<unsigned char>(clamped * 255.0f + 0.5f);
}
inline float to_f01(unsigned char x)
{
return static_cast<float>(x) / 255.0f;
}
} // namespace
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b)
{
::filament_mixer::lerp(r1, g1, b1, r2, g2, b2, t, out_r, out_g, out_b);
}
void filament_mixer_lerp_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b)
{
unsigned char ur = 0, ug = 0, ub = 0;
filament_mixer_lerp(to_u8(r1), to_u8(g1), to_u8(b1),
to_u8(r2), to_u8(g2), to_u8(b2),
t, &ur, &ug, &ub);
*out_r = to_f01(ur);
*out_g = to_f01(ug);
*out_b = to_f01(ub);
}
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b)
{
const float sr1 = linear_to_srgb(clamp01(r1));
const float sg1 = linear_to_srgb(clamp01(g1));
const float sb1 = linear_to_srgb(clamp01(b1));
const float sr2 = linear_to_srgb(clamp01(r2));
const float sg2 = linear_to_srgb(clamp01(g2));
const float sb2 = linear_to_srgb(clamp01(b2));
float out_sr = 0.0f, out_sg = 0.0f, out_sb = 0.0f;
filament_mixer_lerp_float(sr1, sg1, sb1, sr2, sg2, sb2, t, &out_sr, &out_sg, &out_sb);
*out_r = srgb_to_linear(clamp01(out_sr));
*out_g = srgb_to_linear(clamp01(out_sg));
*out_b = srgb_to_linear(clamp01(out_sb));
}
} // namespace Slic3r
+23
View File
@@ -0,0 +1,23 @@
#ifndef SLIC3R_FILAMENT_MIXER_H
#define SLIC3R_FILAMENT_MIXER_H
namespace Slic3r {
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b);
void filament_mixer_lerp_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b);
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b);
} // namespace Slic3r
#endif
+819
View File
@@ -0,0 +1,819 @@
/*
* FilamentMixer — Header-only C++ pigment color mixer
*
* Filament mixer implementation using a degree-4 polynomial regression
* trained to approximate Mixbox behavior (Mean Delta-E ~2.07).
* This library does not include Mixbox source code, binaries, or data files.
*
* Usage:
* #include "filament_mixer_model.h"
*
* unsigned char r, g, b;
* filament_mixer::lerp(0, 33, 133, 252, 211, 0, 0.5f, &r, &g, &b);
* // r=47, g=141, b=56 (blue + yellow → green)
*
* No dependencies beyond the C++ standard library.
*
* MIT License
*
* Copyright (c) 2026 Justin Hayes
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef FILAMENT_MIXER_H
#define FILAMENT_MIXER_H
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace filament_mixer {
namespace detail {
// BEGIN AUTO-GENERATED COEFFICIENTS
// Auto-generated by scripts/export_poly_coefficients.py
// Do not edit manually.
// Degree-4 polynomial, 330 features, 7 inputs
static const int POLY_DEGREE = 4;
static const int N_FEATURES = 330;
static const int N_INPUTS = 7;
static const int POWERS[330][7] = {
{0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 1},
{2, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0},
{1, 0, 1, 0, 0, 0, 0},
{1, 0, 0, 1, 0, 0, 0},
{1, 0, 0, 0, 1, 0, 0},
{1, 0, 0, 0, 0, 1, 0},
{1, 0, 0, 0, 0, 0, 1},
{0, 2, 0, 0, 0, 0, 0},
{0, 1, 1, 0, 0, 0, 0},
{0, 1, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 1, 0, 0},
{0, 1, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1},
{0, 0, 2, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 0, 1, 0, 0},
{0, 0, 1, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 1},
{0, 0, 0, 2, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0},
{0, 0, 0, 1, 0, 1, 0},
{0, 0, 0, 1, 0, 0, 1},
{0, 0, 0, 0, 2, 0, 0},
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 1, 0, 1},
{0, 0, 0, 0, 0, 2, 0},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 2},
{3, 0, 0, 0, 0, 0, 0},
{2, 1, 0, 0, 0, 0, 0},
{2, 0, 1, 0, 0, 0, 0},
{2, 0, 0, 1, 0, 0, 0},
{2, 0, 0, 0, 1, 0, 0},
{2, 0, 0, 0, 0, 1, 0},
{2, 0, 0, 0, 0, 0, 1},
{1, 2, 0, 0, 0, 0, 0},
{1, 1, 1, 0, 0, 0, 0},
{1, 1, 0, 1, 0, 0, 0},
{1, 1, 0, 0, 1, 0, 0},
{1, 1, 0, 0, 0, 1, 0},
{1, 1, 0, 0, 0, 0, 1},
{1, 0, 2, 0, 0, 0, 0},
{1, 0, 1, 1, 0, 0, 0},
{1, 0, 1, 0, 1, 0, 0},
{1, 0, 1, 0, 0, 1, 0},
{1, 0, 1, 0, 0, 0, 1},
{1, 0, 0, 2, 0, 0, 0},
{1, 0, 0, 1, 1, 0, 0},
{1, 0, 0, 1, 0, 1, 0},
{1, 0, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 2, 0, 0},
{1, 0, 0, 0, 1, 1, 0},
{1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 0, 2, 0},
{1, 0, 0, 0, 0, 1, 1},
{1, 0, 0, 0, 0, 0, 2},
{0, 3, 0, 0, 0, 0, 0},
{0, 2, 1, 0, 0, 0, 0},
{0, 2, 0, 1, 0, 0, 0},
{0, 2, 0, 0, 1, 0, 0},
{0, 2, 0, 0, 0, 1, 0},
{0, 2, 0, 0, 0, 0, 1},
{0, 1, 2, 0, 0, 0, 0},
{0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 1, 0, 0},
{0, 1, 1, 0, 0, 1, 0},
{0, 1, 1, 0, 0, 0, 1},
{0, 1, 0, 2, 0, 0, 0},
{0, 1, 0, 1, 1, 0, 0},
{0, 1, 0, 1, 0, 1, 0},
{0, 1, 0, 1, 0, 0, 1},
{0, 1, 0, 0, 2, 0, 0},
{0, 1, 0, 0, 1, 1, 0},
{0, 1, 0, 0, 1, 0, 1},
{0, 1, 0, 0, 0, 2, 0},
{0, 1, 0, 0, 0, 1, 1},
{0, 1, 0, 0, 0, 0, 2},
{0, 0, 3, 0, 0, 0, 0},
{0, 0, 2, 1, 0, 0, 0},
{0, 0, 2, 0, 1, 0, 0},
{0, 0, 2, 0, 0, 1, 0},
{0, 0, 2, 0, 0, 0, 1},
{0, 0, 1, 2, 0, 0, 0},
{0, 0, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 0, 1, 0},
{0, 0, 1, 1, 0, 0, 1},
{0, 0, 1, 0, 2, 0, 0},
{0, 0, 1, 0, 1, 1, 0},
{0, 0, 1, 0, 1, 0, 1},
{0, 0, 1, 0, 0, 2, 0},
{0, 0, 1, 0, 0, 1, 1},
{0, 0, 1, 0, 0, 0, 2},
{0, 0, 0, 3, 0, 0, 0},
{0, 0, 0, 2, 1, 0, 0},
{0, 0, 0, 2, 0, 1, 0},
{0, 0, 0, 2, 0, 0, 1},
{0, 0, 0, 1, 2, 0, 0},
{0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 1, 1, 0, 1},
{0, 0, 0, 1, 0, 2, 0},
{0, 0, 0, 1, 0, 1, 1},
{0, 0, 0, 1, 0, 0, 2},
{0, 0, 0, 0, 3, 0, 0},
{0, 0, 0, 0, 2, 1, 0},
{0, 0, 0, 0, 2, 0, 1},
{0, 0, 0, 0, 1, 2, 0},
{0, 0, 0, 0, 1, 1, 1},
{0, 0, 0, 0, 1, 0, 2},
{0, 0, 0, 0, 0, 3, 0},
{0, 0, 0, 0, 0, 2, 1},
{0, 0, 0, 0, 0, 1, 2},
{0, 0, 0, 0, 0, 0, 3},
{4, 0, 0, 0, 0, 0, 0},
{3, 1, 0, 0, 0, 0, 0},
{3, 0, 1, 0, 0, 0, 0},
{3, 0, 0, 1, 0, 0, 0},
{3, 0, 0, 0, 1, 0, 0},
{3, 0, 0, 0, 0, 1, 0},
{3, 0, 0, 0, 0, 0, 1},
{2, 2, 0, 0, 0, 0, 0},
{2, 1, 1, 0, 0, 0, 0},
{2, 1, 0, 1, 0, 0, 0},
{2, 1, 0, 0, 1, 0, 0},
{2, 1, 0, 0, 0, 1, 0},
{2, 1, 0, 0, 0, 0, 1},
{2, 0, 2, 0, 0, 0, 0},
{2, 0, 1, 1, 0, 0, 0},
{2, 0, 1, 0, 1, 0, 0},
{2, 0, 1, 0, 0, 1, 0},
{2, 0, 1, 0, 0, 0, 1},
{2, 0, 0, 2, 0, 0, 0},
{2, 0, 0, 1, 1, 0, 0},
{2, 0, 0, 1, 0, 1, 0},
{2, 0, 0, 1, 0, 0, 1},
{2, 0, 0, 0, 2, 0, 0},
{2, 0, 0, 0, 1, 1, 0},
{2, 0, 0, 0, 1, 0, 1},
{2, 0, 0, 0, 0, 2, 0},
{2, 0, 0, 0, 0, 1, 1},
{2, 0, 0, 0, 0, 0, 2},
{1, 3, 0, 0, 0, 0, 0},
{1, 2, 1, 0, 0, 0, 0},
{1, 2, 0, 1, 0, 0, 0},
{1, 2, 0, 0, 1, 0, 0},
{1, 2, 0, 0, 0, 1, 0},
{1, 2, 0, 0, 0, 0, 1},
{1, 1, 2, 0, 0, 0, 0},
{1, 1, 1, 1, 0, 0, 0},
{1, 1, 1, 0, 1, 0, 0},
{1, 1, 1, 0, 0, 1, 0},
{1, 1, 1, 0, 0, 0, 1},
{1, 1, 0, 2, 0, 0, 0},
{1, 1, 0, 1, 1, 0, 0},
{1, 1, 0, 1, 0, 1, 0},
{1, 1, 0, 1, 0, 0, 1},
{1, 1, 0, 0, 2, 0, 0},
{1, 1, 0, 0, 1, 1, 0},
{1, 1, 0, 0, 1, 0, 1},
{1, 1, 0, 0, 0, 2, 0},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 2},
{1, 0, 3, 0, 0, 0, 0},
{1, 0, 2, 1, 0, 0, 0},
{1, 0, 2, 0, 1, 0, 0},
{1, 0, 2, 0, 0, 1, 0},
{1, 0, 2, 0, 0, 0, 1},
{1, 0, 1, 2, 0, 0, 0},
{1, 0, 1, 1, 1, 0, 0},
{1, 0, 1, 1, 0, 1, 0},
{1, 0, 1, 1, 0, 0, 1},
{1, 0, 1, 0, 2, 0, 0},
{1, 0, 1, 0, 1, 1, 0},
{1, 0, 1, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 2, 0},
{1, 0, 1, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 0, 2},
{1, 0, 0, 3, 0, 0, 0},
{1, 0, 0, 2, 1, 0, 0},
{1, 0, 0, 2, 0, 1, 0},
{1, 0, 0, 2, 0, 0, 1},
{1, 0, 0, 1, 2, 0, 0},
{1, 0, 0, 1, 1, 1, 0},
{1, 0, 0, 1, 1, 0, 1},
{1, 0, 0, 1, 0, 2, 0},
{1, 0, 0, 1, 0, 1, 1},
{1, 0, 0, 1, 0, 0, 2},
{1, 0, 0, 0, 3, 0, 0},
{1, 0, 0, 0, 2, 1, 0},
{1, 0, 0, 0, 2, 0, 1},
{1, 0, 0, 0, 1, 2, 0},
{1, 0, 0, 0, 1, 1, 1},
{1, 0, 0, 0, 1, 0, 2},
{1, 0, 0, 0, 0, 3, 0},
{1, 0, 0, 0, 0, 2, 1},
{1, 0, 0, 0, 0, 1, 2},
{1, 0, 0, 0, 0, 0, 3},
{0, 4, 0, 0, 0, 0, 0},
{0, 3, 1, 0, 0, 0, 0},
{0, 3, 0, 1, 0, 0, 0},
{0, 3, 0, 0, 1, 0, 0},
{0, 3, 0, 0, 0, 1, 0},
{0, 3, 0, 0, 0, 0, 1},
{0, 2, 2, 0, 0, 0, 0},
{0, 2, 1, 1, 0, 0, 0},
{0, 2, 1, 0, 1, 0, 0},
{0, 2, 1, 0, 0, 1, 0},
{0, 2, 1, 0, 0, 0, 1},
{0, 2, 0, 2, 0, 0, 0},
{0, 2, 0, 1, 1, 0, 0},
{0, 2, 0, 1, 0, 1, 0},
{0, 2, 0, 1, 0, 0, 1},
{0, 2, 0, 0, 2, 0, 0},
{0, 2, 0, 0, 1, 1, 0},
{0, 2, 0, 0, 1, 0, 1},
{0, 2, 0, 0, 0, 2, 0},
{0, 2, 0, 0, 0, 1, 1},
{0, 2, 0, 0, 0, 0, 2},
{0, 1, 3, 0, 0, 0, 0},
{0, 1, 2, 1, 0, 0, 0},
{0, 1, 2, 0, 1, 0, 0},
{0, 1, 2, 0, 0, 1, 0},
{0, 1, 2, 0, 0, 0, 1},
{0, 1, 1, 2, 0, 0, 0},
{0, 1, 1, 1, 1, 0, 0},
{0, 1, 1, 1, 0, 1, 0},
{0, 1, 1, 1, 0, 0, 1},
{0, 1, 1, 0, 2, 0, 0},
{0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 0, 1},
{0, 1, 1, 0, 0, 2, 0},
{0, 1, 1, 0, 0, 1, 1},
{0, 1, 1, 0, 0, 0, 2},
{0, 1, 0, 3, 0, 0, 0},
{0, 1, 0, 2, 1, 0, 0},
{0, 1, 0, 2, 0, 1, 0},
{0, 1, 0, 2, 0, 0, 1},
{0, 1, 0, 1, 2, 0, 0},
{0, 1, 0, 1, 1, 1, 0},
{0, 1, 0, 1, 1, 0, 1},
{0, 1, 0, 1, 0, 2, 0},
{0, 1, 0, 1, 0, 1, 1},
{0, 1, 0, 1, 0, 0, 2},
{0, 1, 0, 0, 3, 0, 0},
{0, 1, 0, 0, 2, 1, 0},
{0, 1, 0, 0, 2, 0, 1},
{0, 1, 0, 0, 1, 2, 0},
{0, 1, 0, 0, 1, 1, 1},
{0, 1, 0, 0, 1, 0, 2},
{0, 1, 0, 0, 0, 3, 0},
{0, 1, 0, 0, 0, 2, 1},
{0, 1, 0, 0, 0, 1, 2},
{0, 1, 0, 0, 0, 0, 3},
{0, 0, 4, 0, 0, 0, 0},
{0, 0, 3, 1, 0, 0, 0},
{0, 0, 3, 0, 1, 0, 0},
{0, 0, 3, 0, 0, 1, 0},
{0, 0, 3, 0, 0, 0, 1},
{0, 0, 2, 2, 0, 0, 0},
{0, 0, 2, 1, 1, 0, 0},
{0, 0, 2, 1, 0, 1, 0},
{0, 0, 2, 1, 0, 0, 1},
{0, 0, 2, 0, 2, 0, 0},
{0, 0, 2, 0, 1, 1, 0},
{0, 0, 2, 0, 1, 0, 1},
{0, 0, 2, 0, 0, 2, 0},
{0, 0, 2, 0, 0, 1, 1},
{0, 0, 2, 0, 0, 0, 2},
{0, 0, 1, 3, 0, 0, 0},
{0, 0, 1, 2, 1, 0, 0},
{0, 0, 1, 2, 0, 1, 0},
{0, 0, 1, 2, 0, 0, 1},
{0, 0, 1, 1, 2, 0, 0},
{0, 0, 1, 1, 1, 1, 0},
{0, 0, 1, 1, 1, 0, 1},
{0, 0, 1, 1, 0, 2, 0},
{0, 0, 1, 1, 0, 1, 1},
{0, 0, 1, 1, 0, 0, 2},
{0, 0, 1, 0, 3, 0, 0},
{0, 0, 1, 0, 2, 1, 0},
{0, 0, 1, 0, 2, 0, 1},
{0, 0, 1, 0, 1, 2, 0},
{0, 0, 1, 0, 1, 1, 1},
{0, 0, 1, 0, 1, 0, 2},
{0, 0, 1, 0, 0, 3, 0},
{0, 0, 1, 0, 0, 2, 1},
{0, 0, 1, 0, 0, 1, 2},
{0, 0, 1, 0, 0, 0, 3},
{0, 0, 0, 4, 0, 0, 0},
{0, 0, 0, 3, 1, 0, 0},
{0, 0, 0, 3, 0, 1, 0},
{0, 0, 0, 3, 0, 0, 1},
{0, 0, 0, 2, 2, 0, 0},
{0, 0, 0, 2, 1, 1, 0},
{0, 0, 0, 2, 1, 0, 1},
{0, 0, 0, 2, 0, 2, 0},
{0, 0, 0, 2, 0, 1, 1},
{0, 0, 0, 2, 0, 0, 2},
{0, 0, 0, 1, 3, 0, 0},
{0, 0, 0, 1, 2, 1, 0},
{0, 0, 0, 1, 2, 0, 1},
{0, 0, 0, 1, 1, 2, 0},
{0, 0, 0, 1, 1, 1, 1},
{0, 0, 0, 1, 1, 0, 2},
{0, 0, 0, 1, 0, 3, 0},
{0, 0, 0, 1, 0, 2, 1},
{0, 0, 0, 1, 0, 1, 2},
{0, 0, 0, 1, 0, 0, 3},
{0, 0, 0, 0, 4, 0, 0},
{0, 0, 0, 0, 3, 1, 0},
{0, 0, 0, 0, 3, 0, 1},
{0, 0, 0, 0, 2, 2, 0},
{0, 0, 0, 0, 2, 1, 1},
{0, 0, 0, 0, 2, 0, 2},
{0, 0, 0, 0, 1, 3, 0},
{0, 0, 0, 0, 1, 2, 1},
{0, 0, 0, 0, 1, 1, 2},
{0, 0, 0, 0, 1, 0, 3},
{0, 0, 0, 0, 0, 4, 0},
{0, 0, 0, 0, 0, 3, 1},
{0, 0, 0, 0, 0, 2, 2},
{0, 0, 0, 0, 0, 1, 3},
{0, 0, 0, 0, 0, 0, 4}
};
static const double COEF[330][3] = {
{8.70954844857314666e-12, 1.27926950848359881e-09, -2.06865474316332923e-09},
{1.05783308354771544e+00, -8.02119209663359686e-03, -7.88705651445470723e-02},
{1.35905954452774837e-02, 8.71267975138422468e-01, 1.04898760410704936e-01},
{-4.16452026099768252e-02, 1.75465381596434100e-02, 1.00224594702931546e+00},
{4.50321316661211821e-02, -7.11409155427628892e-02, 3.91232300778902690e-03},
{1.76675507851922452e-02, -1.32709276116036640e-01, 6.36935270589509828e-02},
{-5.23434830565911030e-02, 3.77681739012521722e-02, -2.08691145087504179e-02},
{-2.33722556520224792e-03, -1.57542611462692145e-03, -3.05158628452478807e-03},
{-8.87678609044812990e-04, 3.83194388837734693e-04, 1.37779212442523083e-03},
{-2.11519042076831979e-03, 5.82337362515735358e-04, 2.24055108941204821e-04},
{4.61545125563611917e-04, 7.72869451707915893e-04, -1.10800630143346882e-03},
{1.05937484157345879e-03, -3.14448681732842211e-04, -1.75129182446198098e-03},
{1.49045689016363055e-03, -2.09220860101674106e-04, 5.93100338908187697e-04},
{-3.51246656293852696e-04, -8.20743017485394289e-04, 5.71854064480802862e-04},
{-9.18204643629581319e-01, -2.27788122702773155e-01, 6.39980793022790623e-02},
{9.24243491377523679e-05, 7.32841332381495400e-04, -1.55219718415109450e-03},
{7.13695056804217989e-04, -8.46467621879685712e-05, 6.50202947442505750e-04},
{1.66640864747485983e-03, -1.24492362771216523e-04, 2.68236502346156410e-04},
{-7.20253644860527516e-04, 7.81434220384157334e-04, 1.12661089007361367e-03},
{-6.83033334365238206e-05, 7.27742627159490762e-04, -1.78048843835204584e-03},
{-3.13431571993316588e-02, -8.57604034845650287e-01, -2.57225920656276863e-01},
{-6.47867200595898341e-05, -1.16688982572457655e-03, 1.14174511750260031e-03},
{-5.00713925613324338e-04, -6.87598082111323477e-04, 6.20598069880440176e-04},
{-8.56716727659588957e-05, 9.74478786593559361e-04, -1.65892838405139512e-03},
{6.53468478750158263e-04, 7.51662000672516676e-04, -6.73196326298856570e-04},
{-4.42539011000103941e-02, -2.01965359697350230e-02, -9.94663493761314355e-01},
{-7.39107395392403087e-04, 5.28870828612476996e-04, 1.00947183860234540e-03},
{-2.06577300933763214e-03, 9.60215813758718011e-04, -3.27993888180819421e-04},
{3.47783280638377555e-04, 8.41824316850705743e-04, -8.87458944147930993e-04},
{1.20960551709587905e+00, -7.07660818059813873e-02, -8.56332806008946491e-03},
{2.11116509318935269e-04, 7.68490846994171776e-04, -1.63228995491542417e-03},
{6.47698075356516103e-04, -4.20589129268072884e-04, 1.18354001300614896e-03},
{-2.78795945253848716e-02, 1.22199201000304547e+00, -2.07383075858847743e-01},
{-5.32457386680677347e-05, -9.58027320315790677e-04, 9.89667309649038679e-04},
{-9.03932426306289782e-02, -4.00969232187064692e-02, 1.26285611182120072e+00},
{-2.19453630740322871e-03, -1.21893190049422620e-03, -1.92293368093085417e-03},
{1.72950845415964505e-06, -8.93952511560151819e-09, -6.14874900641340649e-06},
{8.02644554976326974e-06, -6.42543741723487294e-06, -6.07103419227907060e-06},
{3.20307552755319525e-06, -4.83533743093466500e-06, 9.13563764113473065e-07},
{-2.18105804067510178e-06, 6.19595552598436322e-07, 5.21392855381760945e-06},
{-2.43310123604345563e-06, 2.17201813434465818e-06, 1.94098874242362718e-07},
{-1.56293672065252465e-06, 3.95256011818110372e-06, 1.68792962079201969e-06},
{-1.37567295252127852e-03, 3.59746071987262106e-04, 7.38927139000157259e-05},
{4.27822004137219658e-06, -8.80187479967658548e-07, 2.29453131891411977e-06},
{7.68758937964332534e-06, 2.40909410585557829e-07, 4.69351234070854509e-06},
{-2.87166709944317033e-06, 7.60223902901142716e-07, 4.57864913314467992e-06},
{-4.01295140267654560e-06, 2.65929275888376483e-06, -2.36575067819565221e-06},
{2.32693030513910805e-07, 2.28814396769890308e-06, 1.83526107699893970e-07},
{-2.18213927011287265e-03, 1.65013083920367864e-03, 2.31992998847323087e-04},
{-7.70829764693697905e-06, 4.23888841240673345e-07, 7.30018322002944087e-06},
{-1.23111329452911533e-06, 1.50076529718910084e-06, -1.91139744928209288e-06},
{-1.68872756433485760e-06, 1.03254236824697979e-06, -1.72081108163607555e-06},
{1.64276928199709460e-06, -4.96350219553231067e-07, -1.46349385185670297e-06},
{1.12731767057843682e-03, 5.03104281148445223e-04, 1.36398977654308994e-03},
{-1.05449609518089293e-06, -4.06952115309007489e-07, 3.53062441379482783e-06},
{-1.98745923822574166e-06, 4.98021943693208180e-07, 3.92645061370218429e-06},
{-1.55569377977005097e-07, -4.00262856484093037e-07, -2.49609122397048688e-06},
{2.18005022830924673e-03, -4.10275057064835439e-05, -2.59776311836759947e-04},
{5.41337439827552225e-07, -1.88603932528607146e-06, -2.06428606152470051e-06},
{-6.03243799807140491e-06, -3.75067864464502022e-06, -3.05702776851046742e-06},
{2.30038011634901016e-03, -1.32581161861259635e-03, -1.07680096899188406e-03},
{4.46773877910556887e-06, 1.85008408528524772e-08, -2.72851357570281713e-06},
{-1.49177636513049289e-03, -1.91426739654176659e-04, -1.71206384332753194e-03},
{2.31661325589237743e-02, 2.26540538563063554e-01, 5.42330337046266139e-02},
{-1.40563059963100256e-06, -4.50551806294901061e-06, 8.87542894832671347e-06},
{-1.66780916452391459e-06, 4.12065434881171526e-06, -3.55865035776836702e-06},
{2.71536622051954390e-07, -3.08564858926584692e-06, -1.52164363662402047e-06},
{2.66659632027280158e-06, -1.19436686895073481e-06, -3.25738306279285683e-06},
{-1.43666282346327501e-06, -2.51923473623639690e-06, 5.21205120344175876e-06},
{2.82954522469612199e-04, -1.59147454710008968e-03, 1.27685773978167098e-03},
{-3.99471240294241303e-06, 9.97323772325767188e-08, -5.28196823261495307e-06},
{-6.39858432699424995e-06, -4.59897864440506933e-06, -2.39736149785715891e-06},
{2.89457420106498109e-06, -3.10427512149489757e-06, 9.75553221437691631e-07},
{-8.96518259720091581e-07, -5.53996694461914366e-06, 1.03733964032237669e-05},
{8.82130497168875905e-04, -2.33618402105562365e-03, 1.35100410641244379e-03},
{-2.14088521029685841e-06, 2.59005410360388117e-06, -9.78713171504927426e-08},
{-4.50668337071552516e-06, 3.58808570076458002e-06, -1.56159349007541082e-06},
{-1.52345101244247272e-06, 2.21066768791959578e-06, -2.19555898547246775e-06},
{2.07334042074768356e-03, -1.56333498489329517e-03, -5.53762940364141767e-04},
{2.22151748134440108e-06, -4.74729938900429749e-07, -3.46744150304684889e-06},
{2.95389009221172505e-06, -2.96312023445686329e-06, -9.00385068308695580e-07},
{-6.47780848348620771e-04, 2.38772263398574292e-03, -8.93908589731968019e-04},
{9.69501567645025819e-07, 2.41432205872957328e-06, 5.56908291093893837e-07},
{-6.33392066185247586e-04, 2.38613844267241120e-03, -1.05383725637261472e-03},
{6.76250135616376785e-02, -5.57799579151454852e-02, 1.83393652374666566e-01},
{3.53986894266120067e-06, 5.92996717102502093e-06, -7.32378536156402804e-06},
{5.69667193362453916e-06, 1.20219201908705218e-06, -4.56663805956276925e-06},
{7.11494218295222192e-07, 2.93069858359131137e-06, 1.23210839732268429e-07},
{-3.41917893741799928e-06, -1.47435291776966751e-06, 1.07397354370819542e-06},
{7.30931882734254710e-04, 1.15433149094644884e-03, -2.40026982569019722e-03},
{-1.22780859907432871e-06, 2.29287908084027789e-06, 1.84270754640877832e-06},
{7.71579140080615178e-07, 2.92378122615943208e-06, -1.91800935486416413e-07},
{-3.76107279903559188e-07, -1.83159743461489867e-06, 8.17089655984204466e-07},
{-1.10830882430058061e-03, -5.10908079549339251e-04, -1.77835176235151705e-03},
{-1.26839781743699406e-06, -2.86942252006448415e-06, 4.47464983859263005e-06},
{-1.44518716284694482e-06, -7.03360635528004451e-06, 1.04898109513258675e-05},
{-4.98687888007460470e-04, 1.86990180752567262e-03, -1.24341018156770089e-03},
{-2.90479801332704790e-06, -9.24272269110706229e-07, 7.56354222045119151e-07},
{-1.16451534008294149e-03, -2.34216801827852273e-03, 4.91479264672447288e-03},
{-7.70970926241258958e-02, 9.35855573900774423e-02, 1.50623807158846906e-01},
{1.14039905307547484e-06, -1.80664235182388840e-07, -5.15527441317074897e-06},
{7.50559587697416375e-06, -6.23982034686780714e-06, -5.01245198064126721e-06},
{2.37840954889385892e-06, -4.15663063190341991e-06, 1.93118829429697603e-06},
{-1.54903048110950777e-03, 2.65832194444263125e-04, 5.34401520444913940e-04},
{4.00040634507183718e-06, -2.43965474694277443e-06, 2.88683251413283937e-06},
{7.72301916160400559e-06, -9.54300275625495457e-07, 5.50777546561020959e-06},
{-2.28103126593574368e-03, 1.02658341009706066e-03, 1.22010567464172614e-03},
{-6.32818026002207601e-06, 9.83088209200334157e-07, 5.24316808343458507e-06},
{1.37175660779395581e-03, 4.01188715721313943e-04, 7.59370199245276625e-04},
{-3.33184694847917573e-01, 7.82846225823195241e-02, -9.94270054263078074e-02},
{-1.70108770909324636e-06, -5.10749831734438279e-06, 9.80267482880020635e-06},
{-1.79301365419055891e-06, 4.44839673308561508e-06, -3.83837422072638712e-06},
{1.71911692904483371e-04, -1.56077480341044431e-03, 1.30725115579017584e-03},
{-3.55763938679129477e-06, 1.20558966207589408e-06, -5.94340114624253291e-06},
{1.02325453537648178e-03, -1.52640960762801372e-03, 3.10973117856692537e-04},
{3.81842873295820109e-03, -3.02114884453467680e-01, 2.78264587142456665e-01},
{3.46123498726202961e-06, 5.05929187103208375e-06, -6.85764673719752027e-06},
{4.47228353489932293e-04, 9.60672217798415784e-04, -2.19382758010531077e-03},
{2.22711833124298791e-01, -4.14141995162802465e-02, -4.27998216564745015e-01},
{-1.78271151817048783e-03, -9.81039111371464307e-04, -1.37513011841553174e-03},
{3.35305394560947434e-10, -1.26710751613412498e-09, 3.54248685940916630e-09},
{-9.26917423371698135e-09, 6.21190912597491263e-09, 1.86942252233812667e-08},
{-1.56687696151180944e-09, -5.44315731376698864e-09, 1.93822974337010123e-09},
{7.52897716393974292e-10, -3.48923168136394679e-10, -5.94217786087369859e-10},
{2.52116855170569920e-10, -2.48216903975251313e-09, 1.01699001303634518e-09},
{3.72215577457146729e-09, 4.51910314724912610e-10, -6.15361639422218332e-09},
{-2.62088816666700142e-07, 3.23631086683010168e-07, 8.85302852722882894e-07},
{-1.30537319842360944e-08, 1.46808588619151692e-08, 2.67574040702101001e-09},
{-1.23991327621864045e-08, 2.61298349069072344e-08, -4.58919307373337193e-09},
{5.03079244928983371e-09, -6.73783119575777079e-10, -1.13935871848269699e-08},
{9.09065785148488459e-09, -1.04304054004966673e-08, -3.23123813816827976e-09},
{9.55627910137479830e-10, -1.41129563591135820e-08, -1.75594400131373618e-09},
{-1.05549669436946769e-07, 8.47284096194811896e-08, 6.70761880091491625e-07},
{-5.92079330008488114e-10, 6.31702118392141188e-09, -4.51534448719925763e-09},
{-1.04033970327321867e-09, 4.67775485013532943e-09, 2.79348504744758586e-09},
{5.38758108958869997e-09, -9.55380699552144108e-09, 6.16488249338686956e-11},
{1.12057409185073453e-09, -3.00645183748393663e-09, -2.14940637510707688e-09},
{-6.27004681934967278e-07, 8.59159786402940127e-07, 2.73192537668387470e-07},
{7.36784189214745311e-10, -8.12761968838060511e-10, -2.43226564583531868e-09},
{1.25546123497244366e-09, -6.98609614602219153e-10, -5.29894812750786315e-09},
{-8.88351475714088679e-10, 1.37132565025677167e-09, 1.92497813869541012e-09},
{6.10992637326349119e-07, -6.13496367368217277e-07, -2.19901889726877020e-06},
{-8.59090437677068053e-11, 2.72772732179404898e-09, 1.54554039011323141e-09},
{-4.58798915525804318e-10, 4.54384851966693759e-09, 3.63189350816028877e-09},
{9.93115786933340683e-08, 1.63700862245048928e-07, -1.71397937400244449e-07},
{-1.62985361318312982e-09, -3.10762126448649312e-09, 1.76193495557419588e-09},
{6.27207737564569601e-07, -1.49343052365004934e-06, 8.16168870109573730e-08},
{1.42518738380244172e-03, -3.47531891583186285e-04, -2.98661838800559913e-04},
{8.98157254125564464e-09, -8.24242643235328920e-09, -5.34769730234363472e-09},
{-2.17776999489327494e-08, -4.47141107473569832e-09, -1.10218517090920898e-08},
{3.19614509858290319e-09, -3.32861183754973311e-09, 9.92016746526047655e-11},
{-2.91660393059167689e-09, 5.59829099744391101e-09, 1.70080685646389895e-09},
{1.22479524179014421e-09, 9.20737683318684219e-09, -1.10618757209746121e-10},
{7.70594587548882257e-09, -1.33267446898667659e-06, 4.52812675308736368e-07},
{9.46080642993951670e-09, -1.95483249032513129e-08, -1.23592694620255905e-08},
{-2.02330094345448686e-09, 1.18198534293512125e-10, 2.34746776184291406e-09},
{4.00839940406516604e-09, -4.80716730311137042e-09, 5.25802457129742606e-09},
{-2.53115202408782380e-09, 2.05563177591017165e-10, 5.46003270374129102e-09},
{3.24841319972028232e-08, -1.24284705839720552e-06, 4.97326549863015555e-07},
{1.37729661009444726e-09, -1.67903983772088594e-09, -5.62083748989472554e-09},
{-3.53256937590806785e-10, 4.49320892992322030e-09, -4.02300486673778934e-09},
{2.48976475547557641e-09, -6.97256366533061112e-09, 1.43185084622299286e-09},
{-4.38617299338556199e-09, 9.45081248826811111e-08, -2.91197460585562728e-07},
{3.24429103026879773e-09, -1.71647943601749287e-09, 2.71076100455402980e-09},
{3.86933235105302309e-09, -2.82628156988984358e-09, 8.24455756442965537e-09},
{-7.46614068323353530e-07, 1.27696340529665289e-06, 6.88413034833322557e-07},
{-5.78118683480788320e-09, 1.34319005917760137e-09, -1.15898873831454807e-09},
{4.42686972671260670e-07, 6.41810588767341775e-07, -1.16058405342719939e-08},
{2.24399192788231686e-03, -1.35129336477888174e-03, -7.39944244498236844e-04},
{7.47869199901884940e-09, -2.68762612165573955e-09, -7.41584788022109365e-09},
{1.80867308283150230e-09, -2.21500551234043996e-09, 1.86995768869380186e-09},
{-5.05514829302056157e-09, 4.74048706539109688e-09, 2.52998993977016085e-09},
{1.32441967115592973e-09, 5.70339246663831290e-09, 7.13448300437846683e-10},
{1.19767475292940212e-06, 6.72445227582811568e-07, -1.97500319605841551e-06},
{-1.70612399208458498e-09, 1.07145120553653328e-09, 1.73225882249550267e-09},
{1.15369127445807962e-09, -5.80362996549510513e-09, 9.33515653667171819e-10},
{3.38692740520230018e-09, 3.72531013675958533e-09, -3.18062756687886861e-09},
{1.14787653780236421e-06, -1.84917201319622368e-06, -2.44834286920736499e-07},
{1.45558928799083276e-09, 1.12720083267348059e-09, 9.00940544390493869e-10},
{2.09654001104286891e-09, 4.92913422578400429e-09, 3.04938074791039071e-10},
{3.54033623213741155e-07, 1.07259516691213860e-06, -6.03027205987524684e-07},
{-2.72038239157446071e-09, -1.60070143945256760e-09, 6.03853855807301443e-10},
{-2.03235662485238069e-06, -1.03151962834260348e-06, 1.99637918628457062e-06},
{-1.26261175077493210e-03, -4.98503988506484859e-04, -1.03875859619143593e-03},
{6.43182729298530376e-10, 8.01776645076301975e-10, -1.83589794755523172e-09},
{4.01805119037978997e-09, -5.63673552278487477e-10, -1.09102650663883693e-08},
{-1.48648961195707585e-09, 5.01067861508053269e-09, 2.99132781045319263e-09},
{-8.91404754824534629e-07, 7.49163968581634775e-07, 2.12542215183124383e-06},
{2.38642574451608525e-09, -3.47605810802065207e-09, 3.86935566920598717e-10},
{-2.80031986488182838e-09, -4.25160427697246490e-11, 2.24182921879090280e-09},
{-1.26991357818351247e-07, -1.45348284568834647e-07, 5.68792533226815389e-07},
{1.39227229745131353e-09, -1.84849578699353145e-09, 2.24967258190267305e-09},
{-1.15462500328497586e-06, 1.84347590761761086e-06, 3.64918716654494962e-07},
{-2.09357112083411985e-03, 1.60820400301404873e-05, 2.27418117008655948e-04},
{-1.04484803378768198e-08, 4.86043558178828050e-09, 2.00996588123336650e-09},
{1.44040971927772432e-08, 1.42223015309195233e-09, 1.99778974613318283e-09},
{-1.62414574166394599e-07, -1.31976785339561840e-06, 4.43918084507000099e-07},
{3.73061943836905385e-09, 1.00036822436866402e-08, -1.05450977117005351e-09},
{-2.06551932971539565e-07, -9.72167971235462190e-07, 4.28861904300768815e-07},
{-2.16051814014425313e-03, 1.48780488507118812e-03, 7.79940397419977911e-04},
{-4.80544204428667854e-09, -1.09870773590259319e-09, 6.58876991984844174e-09},
{1.31575045692056136e-06, 4.32430764481131318e-07, -1.55255090541518703e-06},
{1.28823975640215602e-03, 4.04521283440268135e-04, 1.76186984141882253e-03},
{-1.09767251093991436e-01, -4.94112205838347640e-02, -5.43102978164306804e-02},
{7.93691223854864347e-10, 1.54639511196208446e-08, -1.71518303448969789e-08},
{2.56523843833456056e-09, -2.31047392329486456e-09, -4.29758133398648601e-09},
{-9.87725901069325118e-09, 4.28127375218245732e-09, 2.02888056355376989e-09},
{3.21762172461603768e-10, -5.82937505211322815e-09, 3.88293127512318037e-09},
{1.63250610252241302e-09, -7.02161705168347083e-09, 3.46592492032893329e-09},
{-1.44272117683086343e-07, -4.40408510988914148e-07, 5.92746408872857344e-07},
{2.71961467235293242e-09, -1.47466668633244868e-08, 2.89637452632884873e-08},
{1.47637712476396399e-08, 1.16406781783262581e-09, 2.04904540557215853e-09},
{-5.53709807865621073e-09, 7.05512286092169205e-09, 1.56159114805820565e-09},
{5.29268649740455288e-09, 2.10616986628942016e-08, -3.03219004488264332e-08},
{1.79978890693655025e-07, 7.95085399132693105e-07, -4.78366567607801940e-07},
{-4.03847393894152251e-10, 2.90357085597214848e-09, 1.12992165623992946e-09},
{2.99031871486832301e-09, -1.37951879780606745e-09, 2.41048263988075107e-09},
{1.26882357398550027e-09, 1.30631467101793852e-09, 7.99574240151201820e-10},
{-1.41169562567489137e-08, 1.27148955713198356e-06, -2.89386439707162157e-07},
{-2.68794415198003733e-09, 8.73673404455654889e-10, 2.89557382238125882e-09},
{-4.90264437380538709e-09, 1.89207244316591527e-09, 2.25393465003165261e-09},
{-3.58274654665979853e-08, 2.91386646529383231e-07, -4.98477764412919022e-08},
{1.65722165851311942e-09, -1.11673743863338615e-09, -4.14131162695952071e-09},
{-1.47751280626939874e-07, -2.41471865000848773e-07, -8.53552350049691100e-07},
{-2.24352957583577790e-04, 1.60900273524284708e-03, -1.32260753549593617e-03},
{2.05497643901431104e-09, 1.38702982710459111e-08, -3.09887516689033582e-09},
{3.39770491949997755e-09, 9.41613393506957053e-09, -7.09844738544518350e-10},
{7.86209687630989862e-10, 1.93556837224662104e-10, -6.58630930350234678e-09},
{-6.86841181152253455e-10, -5.57194149153339424e-09, 1.41214109156129197e-09},
{2.59516074158083754e-07, 1.30703181255419770e-06, -4.02454784192984860e-07},
{-5.79425202262839889e-10, 4.05071760856134944e-09, 3.02384985106929349e-09},
{4.00677924866643664e-09, -2.25614611715219127e-09, 7.52819043214891792e-09},
{2.34003759425061020e-09, 5.27462258592681366e-09, -2.05723854618256041e-10},
{2.29340174767722615e-07, 1.05507868574435809e-06, -4.45904844964539748e-07},
{-3.91634245866523401e-09, 1.07849931763048801e-09, 1.85542686770290288e-09},
{-6.62166513287765213e-09, 3.86355018811013196e-09, -1.87861701195224384e-09},
{1.32112240848469842e-07, 4.39339645861430705e-08, -1.59384598983486336e-06},
{2.02488462108796341e-09, -1.48427112267590644e-09, -4.32055485832805175e-09},
{-4.27701540045566375e-07, -1.46229443391283215e-06, -2.38186369433401879e-07},
{-9.86744509368740232e-04, 1.91104095070606826e-03, -8.17774843405986713e-04},
{2.06891823117949514e-10, -2.64060942556376688e-09, 1.86419366055012858e-09},
{8.33785634979378187e-09, -1.00697171434571686e-08, -2.84106664583116952e-09},
{5.07057938692323518e-09, -9.56246298811080919e-09, -6.33399999117045809e-11},
{-6.78808357162941078e-08, -2.21612941845184680e-07, 9.42031624998063144e-08},
{-3.04300065007145903e-09, 5.64120231083542478e-09, 1.65718606892628628e-09},
{3.76240642807612602e-09, -4.58941407446844529e-09, 5.06162500801821125e-09},
{7.25149885354159363e-07, -1.18149759075966698e-06, -6.82406347277120240e-07},
{-4.84358128605144600e-09, 4.56893046833772853e-09, 2.67044331092591847e-09},
{-2.54939737986958903e-07, -1.06106228658746360e-06, 5.04013386790069795e-07},
{-2.17097468872509735e-03, 1.41624400187313607e-03, 8.11305605779899562e-04},
{2.24635331169675823e-10, -6.02144184513875302e-09, 4.15827878380570226e-09},
{-4.55408258326350790e-09, 6.20319154376325343e-09, 2.08760821823750220e-09},
{2.10871853867367065e-07, -4.29346688506603014e-07, 1.15683623843482186e-07},
{1.00732072683129559e-09, 3.88267751283422058e-11, -6.73798626615873530e-09},
{5.34506627847264326e-09, -8.01262819982717645e-08, 1.60888846226225901e-06},
{5.83419066552946048e-04, -2.36474094848551555e-03, 8.79373865688287898e-04},
{-4.85158746510450101e-10, -6.78789624508624456e-09, 4.95385649168511577e-09},
{3.47485142271342085e-07, 5.60944792101468470e-07, -4.35887910682497548e-07},
{5.75824910919892421e-04, -2.18618554413632388e-03, 1.22736498224538170e-03},
{-2.51838883195707221e-02, -8.23487774284355212e-02, 3.33658831723806573e-02},
{-8.70167529698484543e-09, -1.37080219501928280e-08, 1.80728228771354082e-08},
{-4.67111571644807100e-09, -2.72041008123058425e-09, 7.06648883852523113e-09},
{7.26183221906172727e-10, -6.77816339167414128e-09, 4.52883232651690726e-09},
{5.28852302228433047e-09, 6.47161005340457507e-09, -8.67298467766008940e-09},
{-2.25465519365641853e-07, -6.46057585221293529e-07, 3.48151143400587948e-07},
{-1.30051025504229756e-09, -3.25062288891730944e-09, 2.01775679498084060e-09},
{-5.12724809831333062e-09, 9.33902577666956280e-10, -6.96327353416625883e-10},
{-3.10810940873373909e-09, -7.49756534634826721e-10, 6.87357185058523612e-10},
{-1.52109221995821997e-06, -4.22908767925417317e-07, 1.38629667568307413e-06},
{1.42955317028459206e-09, -7.02968461219199980e-10, -3.81617160094549490e-09},
{2.53707400921232562e-09, -1.60727622877665510e-09, -4.18765366827500429e-09},
{-2.14750738948554787e-07, -6.40554276953864132e-07, 3.76128531993924486e-07},
{3.83073214815787821e-09, 4.50296289838947317e-10, 2.29523194894554194e-09},
{4.76340728555735282e-07, 6.83235613037347367e-07, -4.72205395646296822e-07},
{-6.10651996176347607e-04, -1.06790499934057291e-03, 2.29083496655867842e-03},
{3.95497823379997726e-09, 1.38236928154400474e-09, -6.26218820548585242e-09},
{1.11904936705986557e-09, -1.37869946362223494e-08, -9.34049783699042457e-10},
{1.25499246411697740e-09, -2.73635453185150368e-09, -2.91506864740637139e-09},
{-3.59882924006599270e-07, 1.32511373732895413e-06, -1.55110207063907657e-07},
{1.07068498511608823e-09, 8.92087770321126072e-09, 2.62826524433101838e-10},
{-2.69316546841480431e-09, 9.61138280075601870e-10, 5.19946977139973399e-09},
{-5.92563579700916554e-07, -1.05071339539294234e-06, 1.56249964602256375e-07},
{1.32198180180509439e-09, 5.16087961255351502e-09, 8.46339526239248130e-10},
{2.07323220008381881e-06, 1.02309267446332522e-06, -2.07661522726165781e-06},
{1.31402366846389393e-03, 3.78229792813366064e-04, 1.77496793932758741e-03},
{8.59301428624004160e-10, -6.83071707530125138e-09, 3.36249680876754553e-09},
{5.27310424491833629e-09, 2.09999085065692981e-08, -3.10459945807028959e-08},
{-8.88666080375855039e-08, 4.60897593930476024e-07, 7.41576575386676540e-07},
{-4.85540663230921155e-10, -5.58243438975036810e-09, 7.40450811775872353e-10},
{4.03141117225058743e-07, 1.52035531639227450e-06, 9.06206514897367477e-08},
{5.61075629915620496e-04, -2.05847905628765053e-03, 1.12849817492909434e-03},
{5.11216541321246609e-09, 7.26292920250060092e-09, -8.97145741030058730e-09},
{-4.26211688914213127e-07, -7.03366608210270750e-07, 6.27995585866791828e-07},
{1.15309052943982646e-03, 2.34474318844151959e-03, -4.91856748507475423e-03},
{1.01104427799588961e-01, -4.22361682938472982e-02, -1.88750007538552200e-01},
{3.94738332298860684e-10, -7.81372397340440727e-10, 4.06815717224340290e-09},
{-8.61483928638051566e-09, 5.37427180535843263e-09, 1.81738104426676372e-08},
{-8.48011268844706123e-10, -5.33803143354383280e-09, 2.99703953494934172e-10},
{3.89154099408092063e-07, -2.44166311268514957e-07, -8.03240371135063858e-07},
{-1.20249536439409610e-08, 1.48908931921210019e-08, 1.88292573199966284e-09},
{-1.16401289163015065e-08, 2.57866422936903206e-08, -5.27022399332555125e-09},
{1.37065399911928676e-07, 2.16494406102361175e-08, -7.63924557662179482e-07},
{-6.94754161319199870e-10, 6.65038621394664631e-09, -4.31779645371221932e-09},
{4.72542155592614588e-07, -7.58546986886782931e-07, -2.35913417925837088e-07},
{1.46133817312113241e-03, -3.25193103208258009e-04, -3.06625181254991741e-04},
{9.35794082672593210e-09, -7.92923574022275091e-09, -5.41426242728348939e-09},
{-2.15279239157428748e-08, -4.16754339024882903e-09, -1.12896482995505920e-08},
{2.60645369870582400e-10, 1.44616071127263122e-06, -3.63334053799999057e-07},
{9.17105741349288905e-09, -2.02295233654725681e-08, -1.20002956877085509e-08},
{-1.27759226226098477e-07, 1.28193771791124470e-06, -5.83097827522305323e-07},
{2.26880791869919426e-03, -1.34042850080092401e-03, -7.65092051285704835e-04},
{7.03374036792325796e-09, -2.53508958270032281e-09, -7.66132998708535240e-09},
{-9.71978722189015265e-07, -5.57836512454779054e-07, 1.96329328074063003e-06},
{-1.26115140811304343e-03, -4.81792074617704632e-04, -1.06803272537897391e-03},
{1.19419564863885497e-01, 5.07766738901840875e-02, 4.87642090320925953e-02},
{1.14090414893297520e-09, 1.56073433760228752e-08, -1.78054684078429726e-08},
{3.03285130343056153e-09, -1.58615337531031741e-09, -4.94928394101368241e-09},
{2.64483280249840080e-07, 2.97155396291660413e-07, -5.41608085095034164e-07},
{2.68757552324139226e-09, -1.41400907649469332e-08, 2.93255796729452456e-08},
{-2.11094617584561828e-07, -6.56355695552793272e-07, 3.72180321686621518e-07},
{-2.55073452371079590e-04, 1.57943859317488818e-03, -1.29154484940938240e-03},
{1.40049266628139435e-09, 1.40747080656922208e-08, -2.58792021839981956e-09},
{-2.12330362681090179e-07, -1.30522733223815968e-06, 5.84417623253341567e-07},
{-9.33144849909676392e-04, 1.90305575962152547e-03, -8.35564417983726418e-04},
{1.81624805201406961e-02, 6.84911174969819458e-02, -2.28291882522520390e-02},
{-8.25231299961259879e-09, -1.40227519596081152e-08, 1.78809529925716415e-08},
{1.90689491530449118e-07, 7.01057736002264065e-07, -4.26430629252294580e-07},
{-5.85146839837499930e-04, -1.07311215649546045e-03, 2.31986890222730339e-03},
{-1.05962397073886522e-01, 5.51532131360410807e-02, 1.87542648909451215e-01},
{-1.37499370823599516e-03, -8.49619409242363438e-04, -1.18180356709159952e-03}
};
static const double INTERCEPT[3] = {
-1.29208772400146188e+00,
6.62251952866635918e+00,
-1.35908984683965173e-01
};
// END AUTO-GENERATED COEFFICIENTS
inline void compute_poly_features(const double x[7], double out[330]) {
for (int i = 0; i < N_FEATURES; ++i) {
double val = 1.0;
for (int j = 0; j < N_INPUTS; ++j) {
if (POWERS[i][j] != 0) {
double base = x[j];
int exp = POWERS[i][j];
// Fast integer exponentiation (max exp = 4)
double p = 1.0;
for (int e = 0; e < exp; ++e)
p *= base;
val *= p;
}
}
out[i] = val;
}
}
} // namespace detail
struct RGB {
unsigned char r, g, b;
};
/**
* Mix two RGB colors using polynomial pigment mixing.
*
* This performs polynomial pigment-style RGB interpolation.
*
* @param r1,g1,b1 First color (0-255)
* @param r2,g2,b2 Second color (0-255)
* @param t Mixing ratio: 0.0 = all color1, 1.0 = all color2
* @param out_r,out_g,out_b Output color (0-255)
*/
inline void lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) {
// Clamp t
if (t <= 0.0f) {
*out_r = r1; *out_g = g1; *out_b = b1;
return;
}
if (t >= 1.0f) {
*out_r = r2; *out_g = g2; *out_b = b2;
return;
}
double x[7] = {
static_cast<double>(r1), static_cast<double>(g1), static_cast<double>(b1),
static_cast<double>(r2), static_cast<double>(g2), static_cast<double>(b2),
static_cast<double>(t)
};
double features[330];
detail::compute_poly_features(x, features);
// Dot product: features @ COEF + INTERCEPT
for (int c = 0; c < 3; ++c) {
double sum = detail::INTERCEPT[c];
for (int i = 0; i < detail::N_FEATURES; ++i) {
sum += features[i] * detail::COEF[i][c];
}
// Clamp to [0, 255] and truncate (matches numpy astype(int) behavior)
int val = static_cast<int>(sum);
if (val < 0) val = 0;
if (val > 255) val = 255;
if (c == 0) *out_r = static_cast<unsigned char>(val);
else if (c == 1) *out_g = static_cast<unsigned char>(val);
else *out_b = static_cast<unsigned char>(val);
}
}
/**
* Convenience overload returning an RGB struct.
*/
inline RGB lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t) {
RGB result;
lerp(r1, g1, b1, r2, g2, b2, t, &result.r, &result.g, &result.b);
return result;
}
} // namespace filament_mixer
#endif // FILAMENT_MIXER_H
+12
View File
@@ -373,6 +373,18 @@ set(SLIC3R_GUI_SOURCES
GUI/FilamentGroupPopup.cpp
GUI/PhysicalPrinterDialog.cpp
GUI/PhysicalPrinterDialog.hpp
GUI/MixedGradientSelector.hpp
GUI/MixedGradientSelector.cpp
GUI/MixedGradientWeightsDialog.hpp
GUI/MixedGradientWeightsDialog.cpp
GUI/MixedFilamentColorMapPanel.hpp
GUI/MixedFilamentColorMapPanel.cpp
GUI/MixedFilamentColorMatchDialog.hpp
GUI/MixedFilamentColorMatchDialog.cpp
GUI/MixedMixPreview.hpp
GUI/MixedMixPreview.cpp
GUI/MixedFilamentConfigPanel.hpp
GUI/MixedFilamentConfigPanel.cpp
GUI/Plater.cpp
GUI/Plater.hpp
GUI/PlateSettingsDialog.cpp
+8 -4
View File
@@ -614,6 +614,9 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
if (shader) {
if (idx == 0) {
int extruder_id = model_volume->extruder_id();
// Clamp to valid range; fall back to extruder 1 on overflow
if (extruder_id <= 0 || extruder_id > (int)extruder_colors.size())
extruder_id = 1;
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]);
if (ban_light) {
@@ -623,7 +626,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
// shader->set_uniform("uniform_color", new_color);
}
else {
if (idx <= extruder_colors.size()) {
if (idx <= (int)extruder_colors.size()) {
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]);
if (ban_light) {
@@ -854,7 +857,7 @@ int GLVolumeCollection::load_wipe_tower_preview(
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
TriangleMesh wipe_tower_shell = make_cube(width, depth, height);
for (int extruder_id : plate_extruders) {
if (extruder_id <= extruder_colors.size())
if (extruder_id >= 1 && extruder_id <= (int)extruder_colors.size())
colors.push_back(extruder_colors[extruder_id - 1]);
else
colors.push_back(extruder_colors[0]);
@@ -895,8 +898,9 @@ int GLVolumeCollection::load_real_wipe_tower_preview(
std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
std::vector<Slic3r::ColorRGBA> colors;
if (!plate_extruders.empty()) {
if (plate_extruders.front() <= extruder_colors.size())
colors.push_back(extruder_colors[plate_extruders.front() - 1]);
const int front_id = plate_extruders.front();
if (front_id >= 1 && front_id <= (int)extruder_colors.size())
colors.push_back(extruder_colors[front_id - 1]);
else
colors.push_back(extruder_colors[0]);
}
+81 -13
View File
@@ -479,20 +479,24 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
}
}
// BBS
static const char* keys[] = { "support_filament", "support_interface_filament"};
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
std::string key = std::string(keys[i]);
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (opt != nullptr) {
if (opt->getInt() > filament_cnt) {
// BBS: Rule 1 — reject out-of-range AND mixed filament IDs in support/wall/infill slots.
// Mixed filaments (virtual IDs > num_phys) cannot be used in these roles; reset to 0.
{
static const char* filament_slot_keys[] = {
"support_filament", "support_interface_filament",
"wall_filament", "sparse_infill_filament", "solid_infill_filament"
};
size_t total = wxGetApp().preset_bundle->total_filament_count();
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
for (auto key : filament_slot_keys) {
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (!opt) continue;
int val = opt->getInt();
bool out_of_range = val > (int)total;
bool is_mixed = (val > (int)num_phys && val <= (int)total);
if (out_of_range || is_mixed) {
DynamicPrintConfig new_conf = *config;
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config();
int new_value = 0;
if (conf_temp != nullptr && conf_temp->has(key)) {
new_value = conf_temp->opt_int(key);
}
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
new_conf.set_key_value(key, new ConfigOptionInt(0));
apply(config, &new_conf);
}
}
@@ -541,6 +545,40 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
// Rule 2 — Local-Z dithering is incompatible with mixed-filament region collapse.
// When dithering_local_z_mode is on, force mixed_filament_region_collapse off.
if (config->has("dithering_local_z_mode") && config->has("mixed_filament_region_collapse") &&
config->opt_bool("dithering_local_z_mode") &&
config->opt_bool("mixed_filament_region_collapse")) {
DynamicPrintConfig new_conf = *config;
new_conf.set_key_value("mixed_filament_region_collapse", new ConfigOptionBool(false));
apply(config, &new_conf);
}
// Rule 3 — One-time warning when Local-Z dithering is enabled alongside variable layer height.
{
static bool s_local_z_varlay_warned = false;
bool dithering_on = config->has("dithering_local_z_mode") &&
config->opt_bool("dithering_local_z_mode");
if (dithering_on && !s_local_z_varlay_warned) {
bool has_var = false;
for (const auto* obj : wxGetApp().plater()->model().objects)
if (obj->layer_height_profile.get().size() > 4) { has_var = true; break; }
if (has_var) {
MessageDialog dlg(m_msg_dlg_parent,
_L("Using variable layer height together with Local-Z dithering "
"may result in poor color mixing quality."),
"", wxICON_WARNING | wxOK);
is_msg_dlg_already_exist = true;
dlg.ShowModal();
is_msg_dlg_already_exist = false;
s_local_z_varlay_warned = true;
}
}
if (!dithering_on)
s_local_z_varlay_warned = false;
}
}
void ConfigManipulation::apply_null_fff_config(DynamicPrintConfig *config, std::vector<std::string> const &keys, std::map<ObjectBase *, ModelConfig *> const &configs)
@@ -978,6 +1016,36 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co
std::string printer_type = wxGetApp().preset_bundle->printers.get_edited_preset().get_printer_type(wxGetApp().preset_bundle);
toggle_line("enable_wrapping_detection", DevPrinterConfigUtil::support_wrapping_detection(printer_type));
// Mixed-filament / dithering visibility rules.
const bool local_z_dithering_on =
config->has("dithering_local_z_mode") && config->option("dithering_local_z_mode") != nullptr &&
config->opt_bool("dithering_local_z_mode");
toggle_line("dithering_local_z_whole_objects", local_z_dithering_on);
toggle_line("dithering_local_z_direct_multicolor", local_z_dithering_on);
// local_z_wipe_tower_purge_lines: only when prime tower + local-Z + non-BBL
toggle_line("local_z_wipe_tower_purge_lines",
config->has("enable_prime_tower") && config->opt_bool("enable_prime_tower") &&
local_z_dithering_on && !is_BBL_Printer);
// mixed_filament_surface_indentation only when bias is enabled
const bool component_bias_enabled =
config->has("mixed_filament_component_bias_enabled") &&
config->option("mixed_filament_component_bias_enabled") != nullptr &&
config->opt_bool("mixed_filament_component_bias_enabled");
toggle_line("mixed_filament_surface_indentation", component_bias_enabled);
// infill override sub-options gated by enable_infill_filament_override
const bool show_infill_filament_override_v =
!is_global_config && have_infill && !bSEMM;
const bool show_infill_filament_details_v =
show_infill_filament_override_v &&
config->has("enable_infill_filament_override") &&
config->option("enable_infill_filament_override") != nullptr &&
config->opt_bool("enable_infill_filament_override");
toggle_line("infill_filament_use_base_first_layers", show_infill_filament_details_v);
toggle_line("infill_filament_use_base_last_layers", show_infill_filament_details_v);
}
void ConfigManipulation::update_print_sla_config(DynamicPrintConfig* config, const bool is_global_config/* = false*/)
+19 -1
View File
@@ -2561,7 +2561,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
if (!model_volume.is_model_part())
continue;
unsigned int filaments_count = (unsigned int)dynamic_cast<const ConfigOptionStrings*>(m_config->option("filament_colour"))->values.size();
unsigned int filament_colour_size = (unsigned int)dynamic_cast<const ConfigOptionStrings*>(m_config->option("filament_colour"))->values.size();
unsigned int filaments_count = std::max(filament_colour_size, (unsigned int)wxGetApp().preset_bundle->total_filament_count());
model_volume.update_extruder_count(filaments_count);
}
}
@@ -2831,6 +2832,23 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
volume->set_sla_shift_z(shift_zs[volume->object_idx()]);
}
// BBS: single-extruder mixed filament risk notification
if (printer_technology == ptFFF && wxGetApp().preset_bundle) {
const size_t total_filaments = wxGetApp().preset_bundle->total_filament_count();
const size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
const bool any_mixed = total_filaments > num_phys;
auto* printer_extruder_id_opt = wxGetApp().preset_bundle->printers.get_edited_preset()
.config.option<ConfigOptionInts>("printer_extruder_id");
const int printer_extruders_count = printer_extruder_id_opt ? (int)printer_extruder_id_opt->values.size() : 1;
auto& nm = *wxGetApp().plater()->get_notification_manager();
if (printer_extruders_count == 1 && any_mixed)
nm.push_notification(NotificationType::BBLSingleExtruderMixedFilamentRisk,
NotificationManager::NotificationLevel::WarningNotificationLevel,
_u8L("Mixed filaments are unreliable on a single-extruder printer."));
else
nm.close_notification_of_type(NotificationType::BBLSingleExtruderMixedFilamentRisk);
}
// BBS
if (printer_technology == ptFFF && m_config->has("filament_colour") && (m_canvas_type != ECanvasType::CanvasAssembleView)) {
// Should the wipe tower be visualized ?
+6
View File
@@ -75,6 +75,7 @@
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "GUI.hpp"
#include "GUI_Utils.hpp"
@@ -2950,6 +2951,11 @@ bool GUI_App::on_init_inner()
// BBS if load user preset failed
//if (loaded_preset_result != 0) {
try {
// Apply the user's auto_generate_gradients preference before load_presets
// triggers PresetBundle::sync_mixed_filaments_from_config, which calls
// MixedFilamentManager::auto_generate. The static atomic defaults to true,
// so without this the preference is ignored on the initial preset load.
MixedFilamentManager::set_auto_generate_enabled(app_config->get_bool("auto_generate_gradients"));
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
// If there are substitutions in system profiles, then a "reconfigure" event shall be triggered, which will force
// installation of a compatible system preset, thus nullifying the system preset substitutions.
+2 -1
View File
@@ -2280,8 +2280,9 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
item_name << " (" + _L("current") + ")";
}
wxBitmap bm = (i == 0 || size_t(i - 1) >= icons.size()) ? wxNullBitmap : *icons[i - 1];
append_menu_item(extruder_selection_menu, wxID_ANY, item_name, "",
[i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, i == 0 ? wxNullBitmap : *icons[i - 1], menu,
[i](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(i); }, bm, menu,
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
}
menu->Append(wxID_ANY, name, extruder_selection_menu, _L("Change Filament"));
+20 -5
View File
@@ -1,4 +1,5 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "GUI_ObjectList.hpp"
#include "GUI_Factories.hpp"
@@ -75,9 +76,20 @@ static DynamicPrintConfig& printer_config()
return wxGetApp().preset_bundle->printers.get_edited_preset().config;
}
static size_t total_filaments_count(size_t physical_count)
{
if (wxGetApp().preset_bundle == nullptr)
return physical_count;
return wxGetApp().preset_bundle->mixed_filaments.total_filaments(physical_count);
}
static int filaments_count()
{
return wxGetApp().filaments_cnt();
if (wxGetApp().preset_bundle == nullptr)
return 0;
return static_cast<int>(total_filaments_count(size_t(std::max(wxGetApp().filaments_cnt(), 0))));
}
static void take_snapshot(const std::string& snapshot_name)
@@ -1000,16 +1012,18 @@ void ObjectList::update_objects_list_filament_column(size_t filaments_count)
if (printer_technology() == ptSLA)
filaments_count = 1;
const size_t total_filaments = total_filaments_count(filaments_count);
m_prevent_update_filament_in_config = true;
// BBS: update extruder values even when filaments_count is 1, because it may be reduced from value greater than 1
// Orca: update extruder values even when total_filaments is 1, because it may be reduced from value greater than 1
if (m_objects)
update_filament_values_for_items(filaments_count);
update_filament_values_for_items(total_filaments);
update_filament_colors();
// set show/hide for this column
set_filament_column_hidden(filaments_count == 1);
set_filament_column_hidden(total_filaments == 1);
//a workaround for a wrong last column width updating under OSX
auto em = em_unit(this);
GetColumn(colEditing)->SetWidth(m_columns_width[colEditing]*em);
@@ -1020,6 +1034,7 @@ void ObjectList::update_objects_list_filament_column(size_t filaments_count)
void ObjectList::update_objects_list_filament_column_when_delete_filament(size_t filament_id, size_t filaments_count, int replace_filament_id)
{
m_prevent_update_filament_in_config = true;
size_t total_filaments = total_filaments_count(filaments_count);
// BBS: update extruder values even when filaments_count is 1, because it may be reduced from value greater than 1
if (m_objects)
@@ -1028,7 +1043,7 @@ void ObjectList::update_objects_list_filament_column_when_delete_filament(size_t
update_filament_colors();
// set show/hide for this column
set_filament_column_hidden(filaments_count == 1);
set_filament_column_hidden(total_filaments == 1);
// a workaround for a wrong last column width updating under OSX
GetColumn(colEditing)->SetWidth(25);
+30 -31
View File
@@ -2809,50 +2809,49 @@ int ObjectTablePanel::init_bitmap()
int ObjectTablePanel::init_filaments_and_colors()
{
//DynamicPrintConfig& global_config = wxGetApp().preset_bundle->prints.get_edited_preset().config;
const DynamicPrintConfig* global_config = m_plater->config();
const std::vector<std::string> filament_presets = wxGetApp().preset_bundle->filament_presets;
m_filaments_count = filament_presets.size();
const std::vector<std::string> filament_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config();
m_filaments_count = filament_colors.size();
if (m_filaments_count <= 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") %m_filaments_count;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") % m_filaments_count;
set_default_filaments_and_colors();
return -1;
}
const ConfigOptionStrings* filament_opt = dynamic_cast<const ConfigOptionStrings*>(global_config->option("filament_colour"));
if (filament_opt == nullptr) {
set_default_filaments_and_colors();
return -1;
}
m_filaments_colors.resize(m_filaments_count);
m_filaments_name.resize(m_filaments_count);
unsigned int color_count = filament_opt->values.size();
if (color_count != m_filaments_count) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", invalid color count:%1%, extruder count: %2%") %color_count %m_filaments_count;
}
unsigned int i = 0;
const size_t physical_count = filament_presets.size();
ColorRGB rgb;
while (i < m_filaments_count) {
const std::string& txt_color = global_config->opt_string("filament_colour", i);
if (i < color_count) {
if (decode_color(txt_color, rgb))
{
m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar());
}
else
{
m_filaments_colors[i] = *wxGREEN;
}
}
else {
for (int i = 0; i < (int)m_filaments_count; ++i) {
if (size_t(i) < filament_colors.size() && decode_color(filament_colors[size_t(i)], rgb))
m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar());
else
m_filaments_colors[i] = *wxGREEN;
if (size_t(i) < physical_count) {
m_filaments_name[i] = wxString(std::to_string(i + 1) + ": " + filament_presets[size_t(i)]);
continue;
}
//parse the filaments
m_filaments_name[i] = wxString(std::to_string(i+1) + ": " + filament_presets[i]);
// Mixed-slot row: walk the manager and find the (physical_count + offset)-th enabled, non-deleted entry.
size_t mixed_offset = 0;
for (const MixedFilament &mf : wxGetApp().preset_bundle->mixed_filaments.mixed_filaments()) {
if (!mf.enabled || mf.deleted)
continue;
if (size_t(i) != physical_count + mixed_offset) {
++mixed_offset;
continue;
}
i++;
m_filaments_name[i] = wxString::Format("%d: Mixed Filament %d (F%u + F%u)",
i + 1, i + 1,
unsigned(mf.component_a), unsigned(mf.component_b));
break;
}
if (m_filaments_name[i].empty())
m_filaments_name[i] = wxString::Format("%d: Filament %d", i + 1, i + 1);
}
return 0;
@@ -179,13 +179,14 @@ void GLGizmoMmuSegmentation::data_changed(bool is_serializing)
ModelObject* model_object = m_c->selection_info()->model_object();
int prev_extruders_count = int(m_extruders_colors.size());
if (prev_extruders_count != wxGetApp().filaments_cnt()) {
if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT))
int cur_filaments_count = int(wxGetApp().preset_bundle->total_filament_count());
if (prev_extruders_count != cur_filaments_count) {
if (cur_filaments_count > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT))
show_notification_extruders_limit_exceeded();
this->init_extruders_data();
// Reinitialize triangle selectors because of change of extruder count need also change the size of GLIndexedVertexArray
if (prev_extruders_count != wxGetApp().filaments_cnt())
if (prev_extruders_count != int(m_extruders_colors.size()))
this->init_model_triangle_selectors();
} else if (wxGetApp().plater()->get_extruders_colors() != m_extruders_colors) {
this->init_extruders_data();
@@ -731,6 +732,8 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
continue;
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0;
extruder_idx = std::min(extruder_idx, (int)m_extruders_colors.size() - 1);
if (extruder_idx < 0) extruder_idx = 0;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
@@ -753,6 +756,7 @@ void GLGizmoMmuSegmentation::update_triangle_selectors_colors()
TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
int extruder_idx = m_volumes_extruder_idxs[i];
int extruder_color_idx = std::max(0, extruder_idx - 1);
extruder_color_idx = std::min(extruder_color_idx, (int)m_extruders_colors.size() - 1);
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end());
@@ -767,7 +771,8 @@ void GLGizmoMmuSegmentation::update_from_model_object(bool first_update)
// Extruder colors need to be reloaded before calling init_model_triangle_selectors to render painted triangles
// using colors from loaded 3MF and not from printer profile in Slicer.
if (int prev_extruders_count = int(m_extruders_colors.size());
prev_extruders_count != wxGetApp().filaments_cnt() || wxGetApp().plater()->get_extruders_colors() != m_extruders_colors)
prev_extruders_count != int(wxGetApp().preset_bundle->total_filament_count()) ||
wxGetApp().plater()->get_extruders_colors() != m_extruders_colors)
this->init_extruders_data();
this->init_model_triangle_selectors();
+3
View File
@@ -2221,6 +2221,9 @@ bool MainFrame::get_enable_slice_status()
}
}
if (enable && m_plater->sidebar().has_broken_mixed_filament())
enable = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable;
return enable;
}
@@ -0,0 +1,810 @@
// MixedFilamentColorMapPanel.cpp
// Extracted verbatim from FullSpectrum Plater.cpp:3087-3781 (Task 17).
#include "MixedFilamentColorMapPanel.hpp"
#include "libslic3r/filament_mixer.h"
#include <wx/dcbuffer.h>
#include <wx/event.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <numeric>
#include <vector>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helpers — free functions used only by this widget.
// Copied verbatim from FullSpectrum Plater.cpp:2443-2640.
// ---------------------------------------------------------------------------
namespace {
wxColour blend_multi_filament_mixer(const std::vector<wxColour> &colors, const std::vector<double> &weights)
{
if (colors.empty() || weights.empty())
return wxColour("#26A69A");
unsigned char out_r = 0;
unsigned char out_g = 0;
unsigned char out_b = 0;
double accumulated_weight = 0.0;
bool has_color = false;
for (size_t i = 0; i < colors.size() && i < weights.size(); ++i) {
const double weight = std::max(0.0, weights[i]);
if (weight <= 0.0)
continue;
const wxColour safe = colors[i].IsOk() ? colors[i] : wxColour("#26A69A");
const unsigned char r = static_cast<unsigned char>(safe.Red());
const unsigned char g = static_cast<unsigned char>(safe.Green());
const unsigned char b = static_cast<unsigned char>(safe.Blue());
if (!has_color) {
out_r = r;
out_g = g;
out_b = b;
accumulated_weight = weight;
has_color = true;
continue;
}
const double new_total = accumulated_weight + weight;
if (new_total <= 0.0)
continue;
const float t = float(weight / new_total);
::Slic3r::filament_mixer_lerp(out_r, out_g, out_b, r, g, b, t, &out_r, &out_g, &out_b);
accumulated_weight = new_total;
}
if (!has_color)
return wxColour("#26A69A");
return wxColour(out_r, out_g, out_b);
}
std::vector<int> normalize_color_match_weights(const std::vector<int> &weights, size_t count)
{
std::vector<int> out = weights;
if (out.size() != count)
out.assign(count, count > 0 ? int(100 / int(count)) : 0);
int sum = 0;
for (int &value : out) {
value = std::max(0, value);
sum += value;
}
if (sum <= 0 && count > 0) {
out.assign(count, 0);
out[0] = 100;
return out;
}
std::vector<double> remainders(count, 0.0);
int assigned = 0;
for (size_t idx = 0; idx < count; ++idx) {
const double exact = 100.0 * double(out[idx]) / double(sum);
out[idx] = int(std::floor(exact));
remainders[idx] = exact - double(out[idx]);
assigned += out[idx];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_remainder = -1.0;
for (size_t idx = 0; idx < remainders.size(); ++idx) {
if (remainders[idx] > best_remainder) {
best_remainder = remainders[idx];
best_idx = idx;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
bool color_match_raw_weights_within_range(const std::vector<double> &weights, int min_component_percent)
{
if (min_component_percent <= 0)
return true;
const double min_allowed = double(std::clamp(min_component_percent, 0, 50));
int active_components = 0;
for (const double weight : weights) {
if (weight <= 1e-4)
continue;
++active_components;
if (weight * 100.0 + 1e-6 < min_allowed)
return false;
}
return active_components >= 2;
}
} // anonymous namespace
// ===========================================================================
// MixedFilamentColorMapPanel — implementation
// Verbatim from FullSpectrum Plater.cpp:3087-3781.
// ===========================================================================
MixedFilamentColorMapPanel::MixedFilamentColorMapPanel(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights,
const wxSize &min_size)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, min_size, wxBORDER_SIMPLE)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(min_size);
m_render_timer.SetOwner(this);
m_colors.reserve(filament_ids.size());
for (const unsigned int filament_id : filament_ids) {
if (filament_id >= 1 && filament_id <= palette.size())
m_colors.emplace_back(palette[filament_id - 1]);
else
m_colors.emplace_back(wxColour("#26A69A"));
}
if (m_colors.empty())
m_colors.emplace_back(wxColour("#26A69A"));
set_normalized_weights(initial_weights, false);
Bind(wxEVT_PAINT, &MixedFilamentColorMapPanel::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &MixedFilamentColorMapPanel::on_left_down, this);
Bind(wxEVT_LEFT_UP, &MixedFilamentColorMapPanel::on_left_up, this);
Bind(wxEVT_MOTION, &MixedFilamentColorMapPanel::on_mouse_move, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedFilamentColorMapPanel::on_capture_lost, this);
Bind(wxEVT_SIZE, &MixedFilamentColorMapPanel::on_size, this);
Bind(wxEVT_TIMER, &MixedFilamentColorMapPanel::on_render_timer, this, m_render_timer.GetId());
}
MixedFilamentColorMapPanel::~MixedFilamentColorMapPanel()
{
if (HasCapture())
ReleaseMouse();
if (m_render_timer.IsRunning())
m_render_timer.Stop();
}
std::vector<int> MixedFilamentColorMapPanel::normalized_weights() const
{
return m_weights;
}
wxColour MixedFilamentColorMapPanel::selected_color() const
{
std::vector<double> weights;
weights.reserve(m_weights.size());
for (const int weight : m_weights)
weights.emplace_back(double(std::max(0, weight)));
return blend_multi_filament_mixer(m_colors, weights);
}
void MixedFilamentColorMapPanel::set_normalized_weights(const std::vector<int> &weights, bool notify)
{
m_weights = normalize_color_match_weights(weights, m_colors.size());
initialize_cursor_from_weights();
Refresh();
if (notify)
emit_changed();
}
void MixedFilamentColorMapPanel::set_min_component_percent(int min_component_percent)
{
const int clamped = std::clamp(min_component_percent, 0, 50);
if (m_min_component_percent == clamped)
return;
m_min_component_percent = clamped;
invalidate_cached_bitmap();
Refresh();
}
// ---------------------------------------------------------------------------
// Private: geometry helpers
// ---------------------------------------------------------------------------
MixedFilamentColorMapPanel::GeometryMode MixedFilamentColorMapPanel::geometry_mode() const
{
if (m_colors.size() <= 1)
return GeometryMode::Point;
if (m_colors.size() == 2)
return GeometryMode::Line;
if (m_colors.size() == 3)
return GeometryMode::Triangle;
if (m_colors.size() == 4)
return GeometryMode::TriangleWithCenter;
return GeometryMode::Radial;
}
wxRect MixedFilamentColorMapPanel::canvas_rect() const
{
const wxSize size = GetClientSize();
return wxRect(0, 0, std::max(1, size.GetWidth()), std::max(1, size.GetHeight()));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::make_vec(double x, double y)
{
return Vec2 { x, y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::add_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return Vec2 { lhs.x + rhs.x, lhs.y + rhs.y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::sub_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return Vec2 { lhs.x - rhs.x, lhs.y - rhs.y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::scale_vec(const Vec2 &value, double factor)
{
return Vec2 { value.x * factor, value.y * factor };
}
double MixedFilamentColorMapPanel::dot_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y;
}
double MixedFilamentColorMapPanel::length_sq(const Vec2 &value)
{
return dot_vec(value, value);
}
double MixedFilamentColorMapPanel::dist_sq(const Vec2 &lhs, const Vec2 &rhs)
{
return length_sq(sub_vec(lhs, rhs));
}
std::array<MixedFilamentColorMapPanel::Vec2, 3> MixedFilamentColorMapPanel::simplex_vertices() const
{
return { make_vec(0.50, 0.05), make_vec(0.08, 0.94), make_vec(0.92, 0.94) };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::simplex_center() const
{
const auto vertices = simplex_vertices();
return make_vec((vertices[0].x + vertices[1].x + vertices[2].x) / 3.0,
(vertices[0].y + vertices[1].y + vertices[2].y) / 3.0);
}
std::vector<MixedFilamentColorMapPanel::AnchorPoint> MixedFilamentColorMapPanel::radial_anchor_points() const
{
std::vector<AnchorPoint> anchors;
const size_t count = m_colors.size();
anchors.reserve(count);
if (count == 0)
return anchors;
if (count == 1) {
anchors.emplace_back(AnchorPoint { 0.5, 0.5 });
return anchors;
}
if (count == 2) {
anchors.emplace_back(AnchorPoint { 0.0, 0.5 });
anchors.emplace_back(AnchorPoint { 1.0, 0.5 });
return anchors;
}
if (count == 3) {
anchors.emplace_back(AnchorPoint { 0.0, 0.5 });
anchors.emplace_back(AnchorPoint { 1.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 1.0 });
return anchors;
}
if (count == 4) {
anchors.emplace_back(AnchorPoint { 0.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 1.0 });
anchors.emplace_back(AnchorPoint { 0.0, 1.0 });
return anchors;
}
constexpr double k_pi = 3.14159265358979323846;
const double center_x = 0.5;
const double center_y = 0.5;
const double radius = 0.45;
for (size_t idx = 0; idx < count; ++idx) {
const double angle = (2.0 * k_pi * double(idx)) / double(count);
anchors.emplace_back(AnchorPoint { center_x + radius * std::cos(angle), center_y + radius * std::sin(angle) });
}
return anchors;
}
std::vector<MixedFilamentColorMapPanel::AnchorPoint> MixedFilamentColorMapPanel::anchor_points() const
{
std::vector<AnchorPoint> anchors;
switch (geometry_mode()) {
case GeometryMode::Point:
anchors.emplace_back(AnchorPoint { 0.5, 0.5 });
break;
case GeometryMode::Line:
anchors.emplace_back(AnchorPoint { 0.06, 0.5 });
anchors.emplace_back(AnchorPoint { 0.94, 0.5 });
break;
case GeometryMode::Triangle: {
const auto vertices = simplex_vertices();
for (const Vec2 &vertex : vertices)
anchors.emplace_back(AnchorPoint { vertex.x, vertex.y });
break;
}
case GeometryMode::TriangleWithCenter: {
const auto vertices = simplex_vertices();
for (const Vec2 &vertex : vertices)
anchors.emplace_back(AnchorPoint { vertex.x, vertex.y });
const Vec2 center = simplex_center();
anchors.emplace_back(AnchorPoint { center.x, center.y });
break;
}
case GeometryMode::Radial:
anchors = radial_anchor_points();
break;
}
return anchors;
}
std::array<double, 3> MixedFilamentColorMapPanel::triangle_barycentric(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
const Vec2 &a = triangle[0];
const Vec2 &b = triangle[1];
const Vec2 &c = triangle[2];
const double denom = ((b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y));
if (std::abs(denom) <= 1e-9)
return { 1.0, 0.0, 0.0 };
const double w0 = ((b.y - c.y) * (point.x - c.x) + (c.x - b.x) * (point.y - c.y)) / denom;
const double w1 = ((c.y - a.y) * (point.x - c.x) + (a.x - c.x) * (point.y - c.y)) / denom;
const double w2 = 1.0 - w0 - w1;
return { w0, w1, w2 };
}
bool MixedFilamentColorMapPanel::point_in_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
const auto barycentric = triangle_barycentric(point, triangle);
constexpr double eps = 1e-6;
return barycentric[0] >= -eps && barycentric[1] >= -eps && barycentric[2] >= -eps;
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::closest_point_on_segment(const Vec2 &point, const Vec2 &start, const Vec2 &end)
{
const Vec2 edge = sub_vec(end, start);
const double edge_len_sq = length_sq(edge);
if (edge_len_sq <= 1e-9)
return start;
const double t = std::clamp(dot_vec(sub_vec(point, start), edge) / edge_len_sq, 0.0, 1.0);
return add_vec(start, scale_vec(edge, t));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::closest_point_on_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
if (point_in_triangle(point, triangle))
return point;
Vec2 best = triangle[0];
double best_dist = std::numeric_limits<double>::max();
for (int edge_idx = 0; edge_idx < 3; ++edge_idx) {
const Vec2 candidate = closest_point_on_segment(point, triangle[edge_idx], triangle[(edge_idx + 1) % 3]);
const double candidate_dist = dist_sq(point, candidate);
if (candidate_dist < best_dist) {
best_dist = candidate_dist;
best = candidate;
}
}
return best;
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::normalized_point_from_mouse(const wxMouseEvent &evt) const
{
const wxRect rect = canvas_rect();
const int width = std::max(1, rect.GetWidth() - 1);
const int height = std::max(1, rect.GetHeight() - 1);
return make_vec(
std::clamp(double(evt.GetX() - rect.GetLeft()) / double(width), 0.0, 1.0),
std::clamp(double(evt.GetY() - rect.GetTop()) / double(height), 0.0, 1.0));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::clamp_point_to_geometry(const Vec2 &point) const
{
switch (geometry_mode()) {
case GeometryMode::Point:
return make_vec(0.5, 0.5);
case GeometryMode::Line:
return make_vec(std::clamp(point.x, 0.0, 1.0), 0.5);
case GeometryMode::Triangle:
case GeometryMode::TriangleWithCenter:
return closest_point_on_triangle(point, simplex_vertices());
case GeometryMode::Radial:
return make_vec(std::clamp(point.x, 0.0, 1.0), std::clamp(point.y, 0.0, 1.0));
}
return point;
}
std::vector<double> MixedFilamentColorMapPanel::simplex_weights_from_pos(const Vec2 &point) const
{
const auto triangle = simplex_vertices();
const Vec2 clamped = closest_point_on_triangle(point, triangle);
const auto barycentric = triangle_barycentric(clamped, triangle);
if (geometry_mode() == GeometryMode::Triangle)
return { std::max(0.0, barycentric[0]), std::max(0.0, barycentric[1]), std::max(0.0, barycentric[2]) };
const double shared = std::max(0.0, std::min({ barycentric[0], barycentric[1], barycentric[2] }));
return {
std::max(0.0, barycentric[0] - shared),
std::max(0.0, barycentric[1] - shared),
std::max(0.0, barycentric[2] - shared),
std::max(0.0, shared * 3.0)
};
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::triangle_point_from_weights() const
{
const auto vertices = simplex_vertices();
double total = 0.0;
for (size_t idx = 0; idx < 3 && idx < m_weights.size(); ++idx)
total += std::max(0, m_weights[idx]);
if (total <= 0.0)
return simplex_center();
Vec2 out = make_vec(0.0, 0.0);
for (size_t idx = 0; idx < 3 && idx < m_weights.size(); ++idx) {
const double weight = double(std::max(0, m_weights[idx])) / total;
out = add_vec(out, scale_vec(vertices[idx], weight));
}
return out;
}
void MixedFilamentColorMapPanel::initialize_cursor_from_grid_search()
{
double best_x = 0.5;
double best_y = 0.5;
double best_error = std::numeric_limits<double>::max();
constexpr int grid = 96;
for (int y_idx = 0; y_idx <= grid; ++y_idx) {
for (int x_idx = 0; x_idx <= grid; ++x_idx) {
const Vec2 point = clamp_point_to_geometry(make_vec(double(x_idx) / double(grid), double(y_idx) / double(grid)));
const std::vector<int> probe = normalized_weights_from_pos(point.x, point.y);
if (probe.size() != m_weights.size())
continue;
double error = 0.0;
for (size_t idx = 0; idx < probe.size(); ++idx) {
const double delta = double(probe[idx] - m_weights[idx]);
error += delta * delta;
}
if (error < best_error) {
best_error = error;
best_x = point.x;
best_y = point.y;
}
}
}
m_cursor_x = best_x;
m_cursor_y = best_y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
}
std::vector<double> MixedFilamentColorMapPanel::raw_weights_from_pos(double normalized_x, double normalized_y) const
{
switch (geometry_mode()) {
case GeometryMode::Point:
return { 1.0 };
case GeometryMode::Line: {
const double t = std::clamp(normalized_x, 0.0, 1.0);
return { 1.0 - t, t };
}
case GeometryMode::Triangle:
case GeometryMode::TriangleWithCenter:
return simplex_weights_from_pos(make_vec(normalized_x, normalized_y));
case GeometryMode::Radial:
break;
}
const std::vector<AnchorPoint> anchors = radial_anchor_points();
std::vector<double> out(anchors.size(), 0.0);
if (anchors.empty())
return out;
constexpr double eps = 1e-8;
size_t exact_idx = size_t(-1);
for (size_t idx = 0; idx < anchors.size(); ++idx) {
const double dx = normalized_x - anchors[idx].x;
const double dy = normalized_y - anchors[idx].y;
const double d2 = dx * dx + dy * dy;
if (d2 <= eps) {
exact_idx = idx;
break;
}
out[idx] = 1.0 / std::max(1e-6, d2);
}
if (exact_idx != size_t(-1)) {
std::fill(out.begin(), out.end(), 0.0);
out[exact_idx] = 1.0;
return out;
}
double sum = 0.0;
for (const double value : out)
sum += value;
if (sum <= 0.0) {
out.assign(out.size(), 0.0);
out[0] = 1.0;
return out;
}
for (double &value : out)
value /= sum;
return out;
}
std::vector<int> MixedFilamentColorMapPanel::normalized_weights_from_pos(double normalized_x, double normalized_y) const
{
std::vector<int> raw_weights;
const std::vector<double> raw = raw_weights_from_pos(normalized_x, normalized_y);
raw_weights.reserve(raw.size());
for (const double value : raw)
raw_weights.emplace_back(std::max(0, int(std::lround(value * 100.0))));
return normalize_color_match_weights(raw_weights, raw.size());
}
void MixedFilamentColorMapPanel::initialize_cursor_from_weights()
{
if (m_weights.empty()) {
m_cursor_x = 0.5;
m_cursor_y = 0.5;
return;
}
switch (geometry_mode()) {
case GeometryMode::Point:
m_cursor_x = 0.5;
m_cursor_y = 0.5;
break;
case GeometryMode::Line: {
const int total = std::accumulate(m_weights.begin(), m_weights.end(), 0);
const double t = total > 0 && m_weights.size() >= 2 ? double(std::max(0, m_weights[1])) / double(total) : 0.5;
m_cursor_x = std::clamp(t, 0.0, 1.0);
m_cursor_y = 0.5;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
break;
}
case GeometryMode::Triangle: {
const Vec2 point = triangle_point_from_weights();
m_cursor_x = point.x;
m_cursor_y = point.y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
break;
}
case GeometryMode::TriangleWithCenter:
case GeometryMode::Radial:
initialize_cursor_from_grid_search();
break;
}
}
// ---------------------------------------------------------------------------
// Private: interaction + rendering
// ---------------------------------------------------------------------------
void MixedFilamentColorMapPanel::emit_changed()
{
wxCommandEvent evt(wxEVT_SLIDER, GetId());
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
void MixedFilamentColorMapPanel::update_from_mouse(const wxMouseEvent &evt, bool notify)
{
const Vec2 point = clamp_point_to_geometry(normalized_point_from_mouse(evt));
m_cursor_x = point.x;
m_cursor_y = point.y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
Refresh();
if (notify)
emit_changed();
}
wxColour MixedFilamentColorMapPanel::canvas_background_color() const
{
return GetBackgroundColour().IsOk() ? GetBackgroundColour() : wxColour(245, 245, 245);
}
bool MixedFilamentColorMapPanel::cached_bitmap_matches(const wxSize &size, const wxColour &background) const
{
return m_cached_bitmap.IsOk() && m_cached_bitmap_size == size && m_cached_background == background;
}
void MixedFilamentColorMapPanel::schedule_cached_bitmap_render()
{
if (!m_render_timer.IsRunning())
m_render_timer.StartOnce(80);
}
void MixedFilamentColorMapPanel::invalidate_cached_bitmap()
{
m_cached_bitmap = wxBitmap();
m_cached_bitmap_size = wxSize();
m_cached_background = wxColour();
}
void MixedFilamentColorMapPanel::render_cached_bitmap(const wxSize &size, const wxColour &background)
{
const int width = size.GetWidth();
const int height = size.GetHeight();
if (width <= 0 || height <= 0)
return;
wxImage image(width, height);
unsigned char *data = image.GetData();
if (data != nullptr) {
for (int y = 0; y < height; ++y) {
const double normalized_y = (height > 1) ? double(y) / double(height - 1) : 0.5;
for (int x = 0; x < width; ++x) {
const double normalized_x = (width > 1) ? double(x) / double(width - 1) : 0.5;
const int data_idx = (y * width + x) * 3;
bool paint_pixel = true;
if (geometry_mode() == GeometryMode::Triangle || geometry_mode() == GeometryMode::TriangleWithCenter)
paint_pixel = point_in_triangle(make_vec(normalized_x, normalized_y), simplex_vertices());
const std::vector<double> raw_weights = raw_weights_from_pos(normalized_x, normalized_y);
wxColour color = paint_pixel ? blend_multi_filament_mixer(m_colors, raw_weights) : background;
if (paint_pixel && m_min_component_percent > 0 &&
!color_match_raw_weights_within_range(raw_weights, m_min_component_percent)) {
const bool stripe = (((x + y) / 8) % 2) == 0;
const double factor = stripe ? 0.12 : 0.38;
color = wxColour(
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Red()) * factor)), 0, 255)),
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Green()) * factor)), 0, 255)),
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Blue()) * factor)), 0, 255)));
}
data[data_idx + 0] = color.Red();
data[data_idx + 1] = color.Green();
data[data_idx + 2] = color.Blue();
}
}
}
m_cached_bitmap = wxBitmap(image);
m_cached_bitmap_size = size;
m_cached_background = background;
}
void MixedFilamentColorMapPanel::draw_cached_bitmap(wxAutoBufferedPaintDC &dc, const wxRect &rect)
{
if (!m_cached_bitmap.IsOk())
return;
if (m_cached_bitmap_size == rect.GetSize()) {
dc.DrawBitmap(m_cached_bitmap, rect.GetLeft(), rect.GetTop(), false);
return;
}
wxMemoryDC memdc;
memdc.SelectObject(m_cached_bitmap);
dc.StretchBlit(rect.GetLeft(), rect.GetTop(), rect.GetWidth(), rect.GetHeight(),
&memdc, 0, 0, m_cached_bitmap_size.GetWidth(), m_cached_bitmap_size.GetHeight());
memdc.SelectObject(wxNullBitmap);
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void MixedFilamentColorMapPanel::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const wxRect rect = canvas_rect();
const int width = rect.GetWidth();
const int height = rect.GetHeight();
if (width <= 0 || height <= 0)
return;
const wxColour background = canvas_background_color();
if (!cached_bitmap_matches(rect.GetSize(), background)) {
if (!m_cached_bitmap.IsOk())
render_cached_bitmap(rect.GetSize(), background);
else
schedule_cached_bitmap_render();
}
draw_cached_bitmap(dc, rect);
if (geometry_mode() == GeometryMode::Triangle || geometry_mode() == GeometryMode::TriangleWithCenter) {
const auto triangle = simplex_vertices();
wxPoint points[3] = {
wxPoint(rect.GetLeft() + int(std::lround(triangle[0].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[0].y * double(std::max(1, height - 1))))),
wxPoint(rect.GetLeft() + int(std::lround(triangle[1].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[1].y * double(std::max(1, height - 1))))),
wxPoint(rect.GetLeft() + int(std::lround(triangle[2].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[2].y * double(std::max(1, height - 1)))))
};
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawPolygon(3, points);
if (geometry_mode() == GeometryMode::TriangleWithCenter) {
const Vec2 center = simplex_center();
const wxPoint center_pt(rect.GetLeft() + int(std::lround(center.x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(center.y * double(std::max(1, height - 1)))));
dc.SetPen(wxPen(wxColour(180, 180, 180), 1, wxPENSTYLE_DOT));
for (const wxPoint &vertex : points)
dc.DrawLine(center_pt, vertex);
}
} else {
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
}
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
const auto anchors = anchor_points();
for (size_t idx = 0; idx < anchors.size() && idx < m_colors.size(); ++idx) {
const int anchor_x = rect.GetLeft() + int(std::lround(anchors[idx].x * double(std::max(1, width - 1))));
const int anchor_y = rect.GetTop() + int(std::lround(anchors[idx].y * double(std::max(1, height - 1))));
dc.SetPen(wxPen(wxColour(30, 30, 30), 1));
dc.SetBrush(wxBrush(m_colors[idx]));
dc.DrawCircle(wxPoint(anchor_x, anchor_y), FromDIP(4));
}
const int cursor_x = rect.GetLeft() + int(std::lround(m_cursor_x * double(std::max(1, width - 1))));
const int cursor_y = rect.GetTop() + int(std::lround(m_cursor_y * double(std::max(1, height - 1))));
dc.SetPen(wxPen(wxColour(255, 255, 255), 3));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(wxPoint(cursor_x, cursor_y), FromDIP(7));
dc.SetPen(wxPen(wxColour(30, 30, 30), 1));
dc.DrawCircle(wxPoint(cursor_x, cursor_y), FromDIP(7));
}
void MixedFilamentColorMapPanel::on_left_down(wxMouseEvent &evt)
{
if (!HasCapture())
CaptureMouse();
m_dragging = true;
update_from_mouse(evt, true);
}
void MixedFilamentColorMapPanel::on_left_up(wxMouseEvent &evt)
{
if (m_dragging)
update_from_mouse(evt, true);
m_dragging = false;
if (HasCapture())
ReleaseMouse();
}
void MixedFilamentColorMapPanel::on_mouse_move(wxMouseEvent &evt)
{
if (m_dragging && evt.LeftIsDown())
update_from_mouse(evt, true);
}
void MixedFilamentColorMapPanel::on_capture_lost(wxMouseCaptureLostEvent &)
{
m_dragging = false;
}
void MixedFilamentColorMapPanel::on_size(wxSizeEvent &evt)
{
if (m_cached_bitmap.IsOk())
schedule_cached_bitmap_render();
Refresh(false);
evt.Skip();
}
void MixedFilamentColorMapPanel::on_render_timer(wxTimerEvent &)
{
const wxRect rect = canvas_rect();
render_cached_bitmap(rect.GetSize(), canvas_background_color());
Refresh(false);
}
} } // namespace Slic3r::GUI
@@ -0,0 +1,138 @@
#pragma once
#include <wx/panel.h>
#include <wx/timer.h>
#include <wx/bitmap.h>
#include <wx/colour.h>
#include <vector>
#include <array>
#include <functional>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// MixedFilamentColorMapPanel
//
// Interactive colour-map widget that lets the user pick a multi-filament
// blend by dragging a cursor across a geometry-specific gradient map.
//
// Extracted verbatim from FullSpectrum Plater.cpp:3087-3781 (Task 17).
// ---------------------------------------------------------------------------
class MixedFilamentColorMapPanel : public wxPanel
{
public:
MixedFilamentColorMapPanel(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights,
const wxSize &min_size);
~MixedFilamentColorMapPanel() override;
// Returns the current normalised per-filament weights (sum == 100).
std::vector<int> normalized_weights() const;
// Returns the blended wxColour that corresponds to the current cursor position.
wxColour selected_color() const;
// Programmatically update weights; notify==true fires wxEVT_SLIDER.
void set_normalized_weights(const std::vector<int> &weights, bool notify);
// Minimum per-component percentage below which the region is dimmed/striped.
void set_min_component_percent(int min_component_percent);
private:
// -----------------------------------------------------------------------
// Private nested types (verbatim from FullSpectrum Plater.cpp:3160-3180)
// -----------------------------------------------------------------------
enum class GeometryMode {
Point,
Line,
Triangle,
TriangleWithCenter,
Radial
};
struct AnchorPoint {
double x { 0.5 };
double y { 0.5 };
};
struct Vec2 {
double x { 0.0 };
double y { 0.0 };
};
// -----------------------------------------------------------------------
// Geometry helpers (all inlined in .cpp)
// -----------------------------------------------------------------------
GeometryMode geometry_mode() const;
wxRect canvas_rect() const;
static Vec2 make_vec(double x, double y);
static Vec2 add_vec(const Vec2 &lhs, const Vec2 &rhs);
static Vec2 sub_vec(const Vec2 &lhs, const Vec2 &rhs);
static Vec2 scale_vec(const Vec2 &value, double factor);
static double dot_vec(const Vec2 &lhs, const Vec2 &rhs);
static double length_sq(const Vec2 &value);
static double dist_sq(const Vec2 &lhs, const Vec2 &rhs);
std::array<Vec2, 3> simplex_vertices() const;
Vec2 simplex_center() const;
std::vector<AnchorPoint> radial_anchor_points() const;
std::vector<AnchorPoint> anchor_points() const;
static std::array<double, 3> triangle_barycentric(const Vec2 &point, const std::array<Vec2, 3> &triangle);
static bool point_in_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle);
static Vec2 closest_point_on_segment(const Vec2 &point, const Vec2 &start, const Vec2 &end);
static Vec2 closest_point_on_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle);
Vec2 normalized_point_from_mouse(const wxMouseEvent &evt) const;
Vec2 clamp_point_to_geometry(const Vec2 &point) const;
std::vector<double> simplex_weights_from_pos(const Vec2 &point) const;
Vec2 triangle_point_from_weights() const;
void initialize_cursor_from_grid_search();
std::vector<double> raw_weights_from_pos(double normalized_x, double normalized_y) const;
std::vector<int> normalized_weights_from_pos(double normalized_x, double normalized_y) const;
void initialize_cursor_from_weights();
// -----------------------------------------------------------------------
// Rendering helpers
// -----------------------------------------------------------------------
void emit_changed();
void update_from_mouse(const wxMouseEvent &evt, bool notify);
wxColour canvas_background_color() const;
bool cached_bitmap_matches(const wxSize &size, const wxColour &background) const;
void schedule_cached_bitmap_render();
void invalidate_cached_bitmap();
void render_cached_bitmap(const wxSize &size, const wxColour &background);
void draw_cached_bitmap(wxAutoBufferedPaintDC &dc, const wxRect &rect);
// -----------------------------------------------------------------------
// wx event handlers
// -----------------------------------------------------------------------
void on_paint(wxPaintEvent &evt);
void on_left_down(wxMouseEvent &evt);
void on_left_up(wxMouseEvent &evt);
void on_mouse_move(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_size(wxSizeEvent &evt);
void on_render_timer(wxTimerEvent &evt);
// -----------------------------------------------------------------------
// Member variables (verbatim from FullSpectrum Plater.cpp:3761-3779)
// -----------------------------------------------------------------------
std::vector<wxColour> m_colors;
std::vector<int> m_weights;
wxBitmap m_cached_bitmap;
wxSize m_cached_bitmap_size;
wxColour m_cached_background;
wxTimer m_render_timer;
int m_min_component_percent { 0 };
double m_cursor_x { 0.5 };
double m_cursor_y { 0.5 };
bool m_dragging { false };
};
} } // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
#pragma once
#include <wx/dialog.h>
#include <wx/clrpicker.h>
#include <wx/colour.h>
#include <wx/gauge.h>
#include <wx/scrolwin.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/timer.h>
#include <wx/wrapsizer.h>
#include <limits>
#include <string>
#include <vector>
#include "GUI_Utils.hpp"
#include "libslic3r/MixedFilament.hpp"
namespace Slic3r { namespace GUI {
class MixedFilamentColorMapPanel; // Task 17
// ---------------------------------------------------------------------------
// MixedColorMatchRecipeResult
//
// Verbatim from FullSpectrum Plater.cpp:230-242.
// Holds the result of a brute-force C(N,2)/C(N,3)/C(N,4) ΔE₀₀ search.
// ---------------------------------------------------------------------------
struct MixedColorMatchRecipeResult
{
bool cancelled = false;
bool valid = false;
unsigned int component_a = 1;
unsigned int component_b = 2;
int mix_b_percent = 50;
std::string manual_pattern;
std::string gradient_component_ids;
std::string gradient_component_weights;
wxColour preview_color = wxColour("#26A69A");
double delta_e = std::numeric_limits<double>::infinity();
};
// Free helper (was declared at Plater.cpp:244-246): launch dialog, return recipe.
MixedColorMatchRecipeResult prompt_best_color_match_recipe(
wxWindow *parent,
const std::vector<std::string> &physical_colors,
const wxColour &initial_color);
// Free helper (was declared at Plater.cpp:251):
// build a MixedFilamentDisplayContext from a flat color vector.
MixedFilamentDisplayContext build_mixed_filament_display_context(
const std::vector<std::string> &physical_colors);
// Free helper (was declared in Plater.cpp anon namespace at 252):
// map a recipe to a swatch colour using the display context.
wxColour compute_color_match_recipe_display_color(
const MixedColorMatchRecipeResult &recipe,
const MixedFilamentDisplayContext &context);
// Free helper (was declared at Plater.cpp:247): ΔE₀₀ between two wxColours.
double color_delta_e00(const wxColour &lhs, const wxColour &rhs);
// ---------------------------------------------------------------------------
// MixedFilamentColorMatchDialog
//
// Extracted verbatim from FullSpectrum Plater.cpp:3782-4288.
// The user types/picks an arbitrary target colour and a brute-force search
// finds the C(N,2)/C(N,3)/C(N,4) recipe that minimises ΔE₀₀ to that target.
// ---------------------------------------------------------------------------
class MixedFilamentColorMatchDialog : public DPIDialog
{
public:
MixedFilamentColorMatchDialog(wxWindow *parent,
const std::vector<std::string> &physical_colors,
const wxColour &initial_color);
~MixedFilamentColorMatchDialog() override;
// Kick off the initial background recipe search (called after ShowModal starts).
void begin_initial_recipe_load();
MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; }
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
// UI helpers
void update_range_label();
void rebuild_presets_ui();
void set_recipe_loading(bool loading, const wxString &message);
void sync_inputs_to_requested();
bool apply_requested_target(const wxColour &requested_target);
bool apply_hex_input(bool show_invalid_error);
void request_recipe_match(const wxColour &requested_target, bool debounce, const wxString &loading_message);
void refresh_selected_recipe();
void launch_recipe_match(size_t request_token, const wxColour &requested_target);
void update_dialog_state();
// Declared in the task spec's public API
void sync_recipe_preview(MixedColorMatchRecipeResult &recipe, const wxColour *requested_target = nullptr);
void handle_recipe_result(size_t request_token, const wxColour &requested_target, MixedColorMatchRecipeResult recipe);
void apply_preset(MixedColorMatchRecipeResult preset);
// Data
std::vector<std::string> m_physical_colors;
MixedFilamentDisplayContext m_display_context;
std::vector<wxColour> m_palette;
std::vector<MixedColorMatchRecipeResult> m_presets;
MixedFilamentColorMapPanel *m_color_map = nullptr;
// Widgets
wxTextCtrl *m_hex_input = nullptr;
wxColourPickerCtrl *m_classic_picker = nullptr;
wxSlider *m_range_slider = nullptr;
wxStaticText *m_range_value = nullptr;
wxStaticText *m_presets_label = nullptr;
wxScrolledWindow *m_presets_host = nullptr;
wxWrapSizer *m_presets_sizer = nullptr;
wxPanel *m_loading_panel = nullptr;
wxStaticText *m_loading_label = nullptr;
wxGauge *m_loading_gauge = nullptr;
wxPanel *m_selected_preview = nullptr;
wxStaticText *m_selected_label = nullptr;
wxPanel *m_recipe_preview = nullptr;
wxStaticText *m_recipe_label = nullptr;
wxStaticText *m_delta_label = nullptr;
wxStaticText *m_error_label = nullptr;
// State
wxColour m_requested_target { wxColour("#26A69A") };
wxColour m_selected_target { wxColour("#26A69A") };
MixedColorMatchRecipeResult m_selected_recipe;
wxTimer m_recipe_timer;
wxTimer m_loading_timer;
wxString m_loading_message;
size_t m_recipe_request_token { 0 };
int m_min_component_percent { 15 };
bool m_has_recipe_result { false };
bool m_recipe_loading { false };
bool m_recipe_refresh_pending { false };
bool m_syncing_inputs { false };
};
} } // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <wx/panel.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/spinctrl.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <vector>
#include <string>
#include <functional>
#include <memory>
#include "libslic3r/MixedFilament.hpp"
namespace Slic3r { namespace GUI {
class MixedMixPreview; // Task 15
class MixedGradientSelector; // Task 16
// ---------------------------------------------------------------------------
// MixedFilamentConfigPanel
//
// Inline per-row editor for a single MixedFilament entry. Composes
// MixedMixPreview, MixedGradientSelector and MixedGradientWeightsDialog.
//
// Extracted from FullSpectrum Plater.cpp:4588-6857.
// ---------------------------------------------------------------------------
class MixedFilamentConfigPanel : public wxPanel
{
public:
using OnChangeFn = std::function<void(const MixedFilament &)>;
MixedFilamentConfigPanel(wxWindow *parent,
size_t mixed_id,
const MixedFilament &mf,
size_t num_physical,
const std::vector<std::string> &physical_colors,
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.
MixedFilament get_mixed_filament() const { return m_mf; }
bool has_changes() const { return m_has_changes; }
static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings);
private:
void build_ui();
void update_preview();
void update_local_z_breakdown();
void update_component_picker_visuals();
// Static helpers — verbatim from FullSpectrum Plater.cpp:4943-6042.
static std::vector<unsigned int> decode_gradient_ids(const std::string &s);
static std::string encode_gradient_ids(const std::vector<unsigned int> &ids);
static std::vector<unsigned int> decode_manual_pattern_ids(const std::string &pattern,
unsigned int a,
unsigned int b,
size_t num_physical,
size_t wall_loops = 0);
static std::vector<int> decode_gradient_weights(const std::string &s, size_t n);
static std::vector<int> normalize_gradient_weights(const std::vector<int> &w, size_t n);
static std::string encode_gradient_weights(const std::vector<int> &w);
static std::vector<unsigned int> build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false);
static std::vector<unsigned int> build_weighted_multi_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights,
size_t max_cycle_limit = 0);
static std::string summarize_sequence(const std::vector<unsigned int> &seq);
static std::string summarize_local_z_breakdown(const MixedFilament &mf,
const std::vector<int> &weights,
const MixedFilamentPreviewSettings &preview_settings);
static std::string blend_from_sequence(const std::vector<std::string> &colors,
const std::vector<unsigned int> &seq,
const std::string &fallback);
static std::vector<double> build_local_z_preview_pass_heights(double nominal_layer_height,
double lower_bound,
double upper_bound,
double preferred_a_height,
double preferred_b_height,
int mix_b_percent,
int max_sublayers_limit);
size_t m_mixed_id;
MixedFilament m_mf;
size_t m_num_physical;
std::vector<std::string> m_physical_colors;
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;
wxChoice *m_choice_b = nullptr;
wxChoice *m_choice_c = nullptr;
wxChoice *m_choice_d = nullptr;
wxPanel *m_picker_a_container = nullptr;
wxPanel *m_picker_b_container = nullptr;
wxPanel *m_picker_c_container = nullptr;
wxPanel *m_picker_d_container = nullptr;
wxPanel *m_picker_a_swatch = nullptr;
wxPanel *m_picker_b_swatch = nullptr;
wxPanel *m_picker_c_swatch = nullptr;
wxPanel *m_picker_d_swatch = nullptr;
wxStaticText *m_picker_a_label = nullptr;
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;
wxStaticText *m_surface_offset_target_label = nullptr;
MixedGradientSelector *m_blend_selector = nullptr;
wxStaticText *m_blend_label = nullptr;
wxTextCtrl *m_pattern_ctrl = nullptr;
wxCheckBox *m_local_z_limit_checkbox = nullptr;
wxSpinCtrl *m_local_z_limit_spin = nullptr;
wxSpinCtrlDouble *m_surface_offset_spin = nullptr;
std::vector<wxButton *> m_pattern_quick_buttons;
MixedMixPreview *m_mix_preview = nullptr;
wxStaticText *m_breakdown_label = nullptr;
wxPanel *m_swatch = nullptr;
std::shared_ptr<std::vector<int>> m_selected_weight_state;
OnChangeFn m_on_change;
};
} } // namespace Slic3r::GUI
+272
View File
@@ -0,0 +1,272 @@
#include "MixedGradientSelector.hpp"
#include "GUI_App.hpp" // wxGetApp() / dark_mode()
#include "I18N.hpp" // _L()
#include "Widgets/Label.hpp" // Label::Body_10
#include "libslic3r/filament_mixer.h" // filament_mixer_lerp
#include <wx/dcbuffer.h>
#include <algorithm>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helper: copied verbatim from FullSpectrum Plater.cpp:2424
// ---------------------------------------------------------------------------
namespace {
wxColour blend_pair_filament_mixer(const wxColour &left, const wxColour &right, float t)
{
const wxColour safe_left = left.IsOk() ? left : wxColour("#26A69A");
const wxColour safe_right = right.IsOk() ? right : wxColour("#26A69A");
unsigned char out_r = static_cast<unsigned char>(safe_left.Red());
unsigned char out_g = static_cast<unsigned char>(safe_left.Green());
unsigned char out_b = static_cast<unsigned char>(safe_left.Blue());
::Slic3r::filament_mixer_lerp(static_cast<unsigned char>(safe_left.Red()),
static_cast<unsigned char>(safe_left.Green()),
static_cast<unsigned char>(safe_left.Blue()),
static_cast<unsigned char>(safe_right.Red()),
static_cast<unsigned char>(safe_right.Green()),
static_cast<unsigned char>(safe_right.Blue()),
std::clamp(t, 0.f, 1.f),
&out_r, &out_g, &out_b);
return wxColour(out_r, out_g, out_b);
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Constructor / destructor
// ---------------------------------------------------------------------------
MixedGradientSelector::MixedGradientSelector(wxWindow *parent,
const wxColour &left,
const wxColour &right,
int value_percent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
, m_left(left)
, m_right(right)
, m_value(std::clamp(value_percent, 0, 100))
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(wxSize(FromDIP(96), FromDIP(12)));
Bind(wxEVT_PAINT, &MixedGradientSelector::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &MixedGradientSelector::on_left_down, this);
Bind(wxEVT_LEFT_UP, &MixedGradientSelector::on_left_up, this);
Bind(wxEVT_MOTION, &MixedGradientSelector::on_mouse_move, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedGradientSelector::on_capture_lost, this);
}
MixedGradientSelector::~MixedGradientSelector()
{
if (HasCapture())
ReleaseMouse();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void MixedGradientSelector::set_colors(const wxColour &left, const wxColour &right)
{
m_left = left;
m_right = right;
m_multi_mode = false;
m_multi_colors.clear();
m_multi_weights.clear();
Refresh();
}
void MixedGradientSelector::set_multi_preview(const std::vector<wxColour> &corner_colors,
const std::vector<int> &weights)
{
m_multi_mode = corner_colors.size() >= 3;
m_multi_colors = corner_colors;
m_multi_weights = weights;
Refresh();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
wxRect MixedGradientSelector::gradient_rect() const
{
const int margin_x = FromDIP(2);
const int margin_y = FromDIP(1);
const wxSize sz = GetClientSize();
return wxRect(margin_x, margin_y,
std::max(1, sz.GetWidth() - margin_x * 2),
std::max(1, sz.GetHeight() - margin_y * 2));
}
int MixedGradientSelector::value_from_x(int x) const
{
const wxRect rect = gradient_rect();
const int min_x = rect.GetLeft();
const int max_x = rect.GetLeft() + rect.GetWidth();
const int clamp_x = std::clamp(x, min_x, max_x);
return ((clamp_x - min_x) * 100 + rect.GetWidth() / 2) / rect.GetWidth();
}
void MixedGradientSelector::update_from_x(int x, bool notify)
{
m_value = value_from_x(x);
Refresh();
if (notify) {
wxCommandEvent evt(wxEVT_SLIDER, GetId());
evt.SetInt(m_value);
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void MixedGradientSelector::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const bool is_dark = wxGetApp().dark_mode();
const wxRect rect = gradient_rect();
if (m_multi_mode && m_multi_colors.size() >= 3) {
const wxPoint tl(rect.GetLeft(), rect.GetTop());
const wxPoint tr(rect.GetRight(), rect.GetTop());
const wxPoint br(rect.GetRight(), rect.GetBottom());
const wxPoint bl(rect.GetLeft(), rect.GetBottom());
const wxPoint cc(rect.GetLeft() + rect.GetWidth() / 2,
rect.GetTop() + rect.GetHeight() / 2);
auto draw_tri = [&dc](const wxColour &color,
const wxPoint &a,
const wxPoint &b,
const wxPoint &c) {
wxPoint pts[3] = { a, b, c };
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(color));
dc.DrawPolygon(3, pts);
};
if (m_multi_colors.size() >= 4) {
draw_tri(m_multi_colors[0], tl, tr, cc);
draw_tri(m_multi_colors[1], tr, br, cc);
draw_tri(m_multi_colors[2], br, bl, cc);
draw_tri(m_multi_colors[3], bl, tl, cc);
} else {
// 3-colour layout: first colour occupies one full side, two others on the opposite corners.
draw_tri(m_multi_colors[0], tl, bl, cc);
draw_tri(m_multi_colors[1], tl, tr, cc);
draw_tri(m_multi_colors[2], bl, br, cc);
}
if (m_multi_weights.size() == m_multi_colors.size()) {
dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20));
dc.SetFont(Label::Body_10);
const int pad = FromDIP(2);
if (m_multi_colors.size() >= 4) {
dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]),
rect.GetLeft() + pad, rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]),
rect.GetRight() - FromDIP(28), rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]),
rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14));
dc.DrawText(wxString::Format("%d%%", m_multi_weights[3]),
rect.GetLeft() + pad, rect.GetBottom() - FromDIP(14));
} else {
dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]),
rect.GetLeft() + pad,
rect.GetTop() + rect.GetHeight() / 2 - FromDIP(6));
dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]),
rect.GetRight() - FromDIP(28), rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]),
rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14));
}
}
} else {
const int w = rect.GetWidth();
const int h = rect.GetHeight();
wxImage img(w, h);
unsigned char *data = img.GetData();
if (data != nullptr) {
for (int x = 0; x < w; ++x) {
const float t = (w > 1) ? float(x) / float(w - 1) : 0.5f;
const wxColour col = blend_pair_filament_mixer(m_left, m_right, t);
const unsigned char r = static_cast<unsigned char>(col.Red());
const unsigned char g = static_cast<unsigned char>(col.Green());
const unsigned char b = static_cast<unsigned char>(col.Blue());
for (int y = 0; y < h; ++y) {
const int idx = (y * w + x) * 3;
data[idx + 0] = r;
data[idx + 1] = g;
data[idx + 2] = b;
}
}
dc.DrawBitmap(wxBitmap(img), rect.GetLeft(), rect.GetTop(), false);
} else {
dc.GradientFillLinear(rect, m_left, m_right, wxEAST);
}
}
dc.SetPen(wxPen(is_dark ? wxColour(100, 100, 106) : wxColour(170, 170, 170), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
if (m_multi_mode) {
dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(30, 30, 30));
dc.SetFont(Label::Body_10);
const wxString hint = _L("Click to edit");
wxSize text_sz = dc.GetTextExtent(hint);
dc.DrawText(hint, rect.GetRight() - text_sz.GetWidth() - FromDIP(4), rect.GetTop() + FromDIP(2));
return;
}
int marker_x = rect.GetLeft() + (rect.GetWidth() * m_value + 50) / 100;
marker_x = std::clamp(marker_x, rect.GetLeft(), rect.GetRight());
dc.SetPen(wxPen(wxColour(255, 255, 255), 3));
dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom());
dc.SetPen(wxPen(wxColour(33, 33, 33), 1));
dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom());
}
void MixedGradientSelector::on_left_down(wxMouseEvent &evt)
{
if (m_multi_mode)
return;
if (!HasCapture())
CaptureMouse();
m_dragging = true;
update_from_x(evt.GetX(), false);
}
void MixedGradientSelector::on_left_up(wxMouseEvent &evt)
{
if (m_multi_mode) {
wxCommandEvent click_evt(wxEVT_BUTTON, GetId());
click_evt.SetEventObject(this);
ProcessWindowEvent(click_evt);
return;
}
if (m_dragging)
update_from_x(evt.GetX(), true);
m_dragging = false;
if (HasCapture())
ReleaseMouse();
}
void MixedGradientSelector::on_mouse_move(wxMouseEvent &evt)
{
if (m_dragging && evt.LeftIsDown())
update_from_x(evt.GetX(), false);
}
void MixedGradientSelector::on_capture_lost(wxMouseCaptureLostEvent &)
{
m_dragging = false;
}
} } // namespace Slic3r::GUI
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <wx/panel.h>
#include <wx/colour.h>
#include <vector>
#include <functional>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// MixedGradientSelector
//
// A small horizontal panel that renders a two-colour gradient (or a
// multi-colour preview in "multi mode") and lets the user drag a marker
// to pick a blend percentage. In multi mode the panel renders coloured
// triangles showing corner weights and emits wxEVT_BUTTON on click so the
// owner can open MixedGradientWeightsDialog.
//
// Extracted from FullSpectrum Plater.cpp:4290-4505.
// ---------------------------------------------------------------------------
class MixedGradientSelector : public wxPanel
{
public:
MixedGradientSelector(wxWindow *parent,
const wxColour &left,
const wxColour &right,
int value_percent);
~MixedGradientSelector() override;
// Current blend value 0-100.
int value() const { return m_value; }
bool is_multi_mode() const { return m_multi_mode; }
// Switch to two-colour gradient mode.
void set_colors(const wxColour &left, const wxColour &right);
// Switch to multi-colour preview mode (>= 3 corner colours required).
void set_multi_preview(const std::vector<wxColour> &corner_colors,
const std::vector<int> &weights);
private:
wxRect gradient_rect() const;
int value_from_x(int x) const;
void update_from_x(int x, bool notify);
void on_paint(wxPaintEvent &evt);
void on_left_down(wxMouseEvent &evt);
void on_left_up(wxMouseEvent &evt);
void on_mouse_move(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
wxColour m_left;
wxColour m_right;
bool m_multi_mode { false };
std::vector<wxColour> m_multi_colors;
std::vector<int> m_multi_weights;
int m_value { 50 };
bool m_dragging { false };
};
} } // namespace Slic3r::GUI
@@ -0,0 +1,150 @@
#include "MixedGradientWeightsDialog.hpp"
#include "MixedFilamentColorMapPanel.hpp"
#include "I18N.hpp" // _L()
#include "Widgets/Label.hpp" // Label::Body_12
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/panel.h>
#include <algorithm>
#include <cmath>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helper: copied verbatim from FullSpectrum Plater.cpp:2558
// ---------------------------------------------------------------------------
namespace {
std::vector<int> normalize_color_match_weights(const std::vector<int> &weights, size_t count)
{
std::vector<int> out = weights;
if (out.size() != count)
out.assign(count, count > 0 ? int(100 / int(count)) : 0);
int sum = 0;
for (int &value : out) {
value = std::max(0, value);
sum += value;
}
if (sum <= 0 && count > 0) {
out.assign(count, 0);
out[0] = 100;
return out;
}
std::vector<double> remainders(count, 0.0);
int assigned = 0;
for (size_t idx = 0; idx < count; ++idx) {
const double exact = 100.0 * double(out[idx]) / double(sum);
out[idx] = int(std::floor(exact));
remainders[idx] = exact - double(out[idx]);
assigned += out[idx];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_remainder = -1.0;
for (size_t idx = 0; idx < remainders.size(); ++idx) {
if (remainders[idx] > best_remainder) {
best_remainder = remainders[idx];
best_idx = idx;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Constructor — verbatim from FullSpectrum Plater.cpp:4506-4583
// ---------------------------------------------------------------------------
MixedGradientWeightsDialog::MixedGradientWeightsDialog(
wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights)
: wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"),
wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
m_colors.reserve(filament_ids.size());
m_weights = normalize_color_match_weights(initial_weights, filament_ids.size());
for (const unsigned int filament_id : filament_ids) {
if (filament_id >= 1 && filament_id <= palette.size())
m_colors.emplace_back(palette[filament_id - 1]);
else
m_colors.emplace_back(wxColour("#26A69A"));
}
if (m_colors.empty())
m_colors.emplace_back(wxColour("#26A69A"));
auto *root = new wxBoxSizer(wxVERTICAL);
auto *hint = new wxStaticText(this, wxID_ANY,
_L("Pick a point in the gradient map to control multi-filament mix."));
root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10));
m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights,
wxSize(FromDIP(240), FromDIP(240)));
root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10));
for (size_t i = 0; i < filament_ids.size(); ++i) {
auto *row = new wxBoxSizer(wxHORIZONTAL);
wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition,
wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE);
chip->SetBackgroundColour(m_colors[i]);
row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
row->Add(new wxStaticText(this, wxID_ANY,
wxString::Format("F%d", int(filament_ids[i]))),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
auto *label = new wxStaticText(this, wxID_ANY,
wxString::Format("%d%%", m_weights[i]));
label->SetFont(Label::Body_12);
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8));
m_weight_labels.emplace_back(label);
}
root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8));
SetSizerAndFit(root);
SetMinSize(wxSize(FromDIP(380),
std::max(GetSize().GetHeight(), FromDIP(460))));
update_weight_labels();
if (m_color_map) {
m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) {
m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights;
update_weight_labels();
});
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
std::vector<int> MixedGradientWeightsDialog::normalized_weights() const
{
return m_color_map ? m_color_map->normalized_weights() : m_weights;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
void MixedGradientWeightsDialog::update_weight_labels()
{
for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) {
if (m_weight_labels[i])
m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i]));
}
Layout();
}
} } // namespace Slic3r::GUI
@@ -0,0 +1,45 @@
#pragma once
#include <wx/dialog.h>
#include <wx/colour.h>
#include <wx/stattext.h>
#include <vector>
namespace Slic3r { namespace GUI {
// Forward-declare Task-17 panel: defined in MixedFilamentColorMapPanel.hpp.
class MixedFilamentColorMapPanel;
// ---------------------------------------------------------------------------
// MixedGradientWeightsDialog
//
// A modal dialog that shows a MixedFilamentColorMapPanel and per-filament
// weight labels so the user can pick multi-filament blend weights for a
// gradient mix.
//
// Extracted from FullSpectrum Plater.cpp:4506-4583.
//
// NOTE (Task 17 dependency): the constructor body that instantiates
// MixedFilamentColorMapPanel is guarded with #if 0 until Task 17 lands.
// See MixedGradientWeightsDialog.cpp for details.
// ---------------------------------------------------------------------------
class MixedGradientWeightsDialog : public wxDialog
{
public:
MixedGradientWeightsDialog(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights);
// Returns the normalised per-filament weight vector chosen by the user.
std::vector<int> normalized_weights() const;
private:
void update_weight_labels();
MixedFilamentColorMapPanel *m_color_map { nullptr };
std::vector<wxColour> m_colors;
std::vector<int> m_weights;
std::vector<wxStaticText *> m_weight_labels;
};
} } // namespace Slic3r::GUI
+181
View File
@@ -0,0 +1,181 @@
#include "MixedMixPreview.hpp"
#include "GUI_App.hpp" // wxGetApp() / dark_mode()
#include <wx/dcbuffer.h> // wxAutoBufferedPaintDC
#include <algorithm>
#include <cmath>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
MixedMixPreview::MixedMixPreview(wxWindow *parent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(wxSize(FromDIP(120), FromDIP(20)));
Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void MixedMixPreview::set_data(const std::vector<wxColour> &palette,
const std::vector<unsigned int> &sequence,
bool same_layer_mode,
const std::vector<double> &surface_offsets_mm,
const wxColour &fallback,
const wxString &left_overlay,
const wxString &right_overlay)
{
m_palette = palette;
m_sequence = sequence;
m_same_layer = same_layer_mode;
m_surface_offsets_mm = surface_offsets_mm;
m_fallback = fallback;
m_left_overlay = left_overlay;
m_right_overlay = right_overlay;
Refresh();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
wxRect MixedMixPreview::preview_rect() const
{
const int margin_x = FromDIP(1);
const int margin_y = FromDIP(1);
const wxSize sz = GetClientSize();
return wxRect(margin_x, margin_y,
std::max(1, sz.GetWidth() - margin_x * 2),
std::max(1, sz.GetHeight() - margin_y * 2));
}
wxColour MixedMixPreview::color_for_extruder(unsigned int extruder_id) const
{
if (extruder_id >= 1 && extruder_id <= m_palette.size())
return m_palette[extruder_id - 1];
return m_fallback;
}
double MixedMixPreview::max_active_surface_offset_mm() const
{
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 MixedMixPreview::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 (std::abs(offset_mm) <= EPSILON)
return 0;
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));
}
// ---------------------------------------------------------------------------
// Paint handler
// ---------------------------------------------------------------------------
void MixedMixPreview::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const wxRect rect = preview_rect();
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_fallback));
dc.DrawRectangle(rect);
if (!m_sequence.empty()) {
if (m_same_layer) {
// Same-layer preview: full-height stripe lines.
const int stripes = 24;
const int stripe_w = std::max(1, rect.GetWidth() / stripes);
const size_t seq_len = m_sequence.size();
for (int s = 0; s < stripes; ++s) {
const size_t idx = size_t(s % int(seq_len));
const unsigned int extruder_id = m_sequence[idx];
dc.SetBrush(wxBrush(color_for_extruder(extruder_id)));
const int x = rect.GetLeft() + s * stripe_w;
const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w;
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;
const int bar_w = std::max(1, rect.GetWidth() / bars);
for (int i = 0; i < bars; ++i) {
size_t idx = 0;
if (m_sequence.size() > size_t(bars))
idx = (size_t(i) * m_sequence.size()) / size_t(bars);
else
idx = size_t(i) % m_sequence.size();
const unsigned int extruder_id = m_sequence[idx];
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 = 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);
}
}
}
auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) {
if (text.empty())
return;
dc.SetTextForeground(wxColour(255, 255, 255));
const int outline_radius = std::max(2, FromDIP(2));
for (int ox = -outline_radius; ox <= outline_radius; ++ox) {
for (int oy = -outline_radius; oy <= outline_radius; ++oy) {
if (ox == 0 && oy == 0)
continue;
dc.DrawText(text, x + ox, y + oy);
}
}
dc.SetTextForeground(wxColour(22, 22, 22));
dc.DrawText(text, x, y);
};
wxCoord left_w = 0, left_h = 0;
wxCoord right_w = 0, right_h = 0;
dc.GetTextExtent(m_left_overlay, &left_w, &left_h);
dc.GetTextExtent(m_right_overlay, &right_w, &right_h);
const int text_y = rect.GetTop()
+ std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2);
const int pad = FromDIP(6);
if (!m_left_overlay.empty())
draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y);
if (!m_right_overlay.empty())
draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y);
const bool is_dark = wxGetApp().dark_mode();
dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
}
} } // namespace Slic3r::GUI
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <wx/panel.h>
#include <wx/colour.h>
#include <vector>
#include <string>
namespace Slic3r { namespace GUI {
// Preview strip that shows the layer-by-layer or same-layer colour sequence
// produced by a mixed filament definition. All logic is self-contained; the
// owning panel calls set_data() whenever the underlying MixedFilament changes.
class MixedMixPreview : public wxPanel
{
public:
explicit MixedMixPreview(wxWindow *parent);
void set_data(const std::vector<wxColour> &palette,
const std::vector<unsigned int> &sequence,
bool same_layer_mode,
const std::vector<double> &surface_offsets_mm,
const wxColour &fallback,
const wxString &left_overlay,
const wxString &right_overlay);
private:
wxRect preview_rect() const;
wxColour color_for_extruder(unsigned int extruder_id) const;
double max_active_surface_offset_mm() const;
int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const;
void on_paint(wxPaintEvent &evt);
std::vector<wxColour> m_palette;
std::vector<unsigned int> m_sequence;
std::vector<double> m_surface_offsets_mm;
bool m_same_layer { false };
wxColour m_fallback { wxColour(38, 166, 154) };
wxString m_left_overlay;
wxString m_right_overlay;
};
} } // namespace Slic3r::GUI
+2
View File
@@ -166,6 +166,8 @@ enum class NotificationType
OrcaSharedProfilesAvailable,
OrcaCloudAPIError,
OrcaSyncConflict,
BBLMixedFilamentBroken,
BBLSingleExtruderMixedFilamentRisk,
NotificationTypeCount
};
+111 -3
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <string>
#include <regex>
#include <sstream>
#include <future>
#include <glad/gl.h>
#include <boost/algorithm/string.hpp>
@@ -23,6 +24,7 @@
#include "libslic3r/Tesselate.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -1619,12 +1621,37 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
return plate_extruders;
// Expand any mixed-filament virtual slots to their physical component extruders
{
const auto& mgr = wxGetApp().preset_bundle->mixed_filaments;
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
std::vector<int> expanded;
for (int e : plate_extruders) {
if (e <= 0) continue;
auto u = static_cast<unsigned int>(e);
if (mgr.is_mixed(u, num_phys)) {
if (auto* mf = mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(static_cast<int>(mf->component_a));
expanded.push_back(static_cast<int>(mf->component_b));
}
} else {
expanded.push_back(e);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
return expanded;
}
}
std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const
{
std::vector<int> plate_extruders;
BOOST_LOG_TRIVIAL(debug) << "PartPlate::get_extruders_under_cli begin"
<< " plate=" << m_plate_index
<< " obj_to_instance_count=" << obj_to_instance_set.size()
<< " consider_custom_gcode=" << conside_custom_gcode;
// if 3mf file
int glb_support_intf_extr = full_config.opt_int("support_interface_filament");
@@ -1644,7 +1671,27 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
if ((obj_id >= 0) && (obj_id < m_model->objects.size()))
{
ModelObject* object = m_model->objects[obj_id];
if (object == nullptr) {
BOOST_LOG_TRIVIAL(error) << "PartPlate::get_extruders_under_cli encountered null model object"
<< " plate=" << m_plate_index
<< " obj_id=" << obj_id;
continue;
}
if (instance_id < 0 || instance_id >= object->instances.size()) {
BOOST_LOG_TRIVIAL(error) << "PartPlate::get_extruders_under_cli encountered invalid instance index"
<< " plate=" << m_plate_index
<< " obj_id=" << obj_id
<< " instance_id=" << instance_id
<< " instance_count=" << object->instances.size();
continue;
}
ModelInstance* instance = object->instances[instance_id];
BOOST_LOG_TRIVIAL(debug) << "PartPlate::get_extruders_under_cli object"
<< " plate=" << m_plate_index
<< " obj_id=" << obj_id
<< " instance_id=" << instance_id
<< " volume_count=" << object->volumes.size()
<< " printable=" << instance->printable;
if (!instance->printable)
continue;
@@ -1741,7 +1788,45 @@ std::vector<int> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, D
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
return plate_extruders;
// Expand any mixed-filament virtual slots to their physical component extruders.
// CLI context: rebuild the manager inline from full_config (no wxGetApp).
{
MixedFilamentManager local_mgr;
std::vector<std::string> filament_colours;
if (const auto* col_opt = dynamic_cast<const ConfigOptionStrings*>(full_config.option("filament_colour")))
filament_colours = col_opt->values;
local_mgr.auto_generate(filament_colours);
if (const auto* defs_opt = dynamic_cast<const ConfigOptionString*>(full_config.option("mixed_filament_definitions")))
if (!defs_opt->value.empty())
local_mgr.load_custom_entries(defs_opt->value, filament_colours);
size_t num_phys = filament_colours.size();
std::vector<int> expanded;
for (int e : plate_extruders) {
if (e <= 0) continue;
auto u = static_cast<unsigned int>(e);
if (local_mgr.is_mixed(u, num_phys)) {
if (auto* mf = local_mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(static_cast<int>(mf->component_a));
expanded.push_back(static_cast<int>(mf->component_b));
}
} else {
expanded.push_back(e);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
std::ostringstream extruders_list;
for (size_t i = 0; i < expanded.size(); ++i) {
if (i != 0)
extruders_list << ",";
extruders_list << expanded[i];
}
BOOST_LOG_TRIVIAL(debug) << "PartPlate::get_extruders_under_cli result"
<< " plate=" << m_plate_index
<< " extruders=[" << extruders_list.str() << "]";
return expanded;
}
}
bool PartPlate::check_objects_empty_and_gcode3mf(std::vector<int> &result) const
@@ -1794,7 +1879,28 @@ std::vector<int> PartPlate::get_extruders_without_support(bool conside_custom_gc
std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(plate_extruders.begin(), plate_extruders.end());
plate_extruders.resize(std::distance(plate_extruders.begin(), it_end));
return plate_extruders;
// Expand any mixed-filament virtual slots to their physical component extruders
{
const auto& mgr = wxGetApp().preset_bundle->mixed_filaments;
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
std::vector<int> expanded;
for (int e : plate_extruders) {
if (e <= 0) continue;
auto u = static_cast<unsigned int>(e);
if (mgr.is_mixed(u, num_phys)) {
if (auto* mf = mgr.mixed_filament_from_id(u, num_phys)) {
expanded.push_back(static_cast<int>(mf->component_a));
expanded.push_back(static_cast<int>(mf->component_b));
}
} else {
expanded.push_back(e);
}
}
std::sort(expanded.begin(), expanded.end());
expanded.erase(std::unique(expanded.begin(), expanded.end()), expanded.end());
return expanded;
}
}
/* -1 is invalid, return physical extruder idx*/
@@ -1821,6 +1927,8 @@ int PartPlate::get_physical_extruder_by_filament_id(const DynamicConfig& g_confi
}
int zero_base_logical_idx = filament_map[idx - 1] - 1;
if (zero_base_logical_idx < 0 || zero_base_logical_idx >= (int)the_map->values.size())
return -1;
return the_map->values[zero_base_logical_idx];
}
+12
View File
@@ -216,6 +216,18 @@ OtherLayersSeqPanel::OtherLayersSeqPanel(wxWindow* parent)
Layout();
top_sizer->Fit(this);
// Disable custom sequence when mixed (virtual) filaments are in use.
{
size_t total = wxGetApp().preset_bundle->total_filament_count();
size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
if (total > num_phys) {
m_other_layer_print_seq_choice->Disable();
auto* warn = new wxStaticText(this, wxID_ANY,
_L("Custom layer sequence is unavailable when mixed filaments are used."));
warn->SetForegroundColour(wxColour(255, 100, 0));
top_sizer->Add(warn, 0, wxALIGN_LEFT | wxTOP, FromDIP(4));
}
}
m_other_layer_print_seq_choice->Bind(wxEVT_COMBOBOX, [this, buttons_sizer](auto& e) {
if (e.GetSelection() == 0) {
+1109 -6
View File
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -182,6 +182,14 @@ public:
void on_filament_count_change(size_t num_filaments);
void on_filaments_delete(size_t filament_id);
// Mixed Filaments panel
void update_mixed_filament_panel(bool sync_manager = true);
std::vector<unsigned int> get_ui_ordered_filament_ids() const;
// Returns true when any mixed filament references a component ID that is
// out of the physical filament range (e.g. after the user reduces the
// physical filament count).
bool has_broken_mixed_filament() const;
void add_filament();
void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default
void change_filament(size_t from_id, size_t to_id); // 0 base
@@ -556,7 +564,18 @@ public:
void on_filament_change(size_t filament_idx);
void on_filament_count_change(size_t extruders_count);
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1);
void on_filaments_delete(size_t extruders_count, size_t filament_id, int replace_filament_id = -1,
const std::vector<unsigned char>& is_mixed_before_delete = {});
// FullSpectrum: gate auto gradient generation when many physical filaments would create a large grid.
// Returns true when callers may proceed with auto-generated gradients. As a side effect, this
// call also sets MixedFilamentManager's static auto-generate flag to match the returned decision,
// so callers do not need to set it themselves. Pops a yes/no dialog at most once per
// physical-filament count (cached per Plater instance).
bool confirm_auto_generated_gradients(size_t num_physical);
// Force a decision into the prompt cache without showing a dialog. Pass num_physical = 0 to
// invalidate the cache (so the next genuine count-growth event re-prompts), or the current
// count to record the user's decision. Used by the Preferences toggle.
void set_auto_generated_gradient_decision(size_t num_physical, bool create_auto_gradients);
std::vector<Slic3r::ColorRGBA> get_extruders_colors();
// BBS
void on_bed_type_change(BedType bed_type);
@@ -568,7 +587,7 @@ public:
void force_print_bed_update();
// On activating the parent window.
void on_activate();
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr) const;
std::vector<std::string> get_extruder_colors_from_plater_config(const GCodeProcessorResult* const result = nullptr, bool include_mixed = true) const;
std::vector<std::string> get_filament_colors_render_info() const;
std::vector<std::string> get_filament_color_render_type() const;
std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
+17
View File
@@ -7,6 +7,7 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/MixedFilament.hpp"
#include <wx/language.h>
#include "OG_CustomCtrl.hpp"
#include "wx/graphics.h"
@@ -1040,6 +1041,19 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
}
}
if (param == "auto_generate_gradients") {
MixedFilamentManager::set_auto_generate_enabled(checkbox->GetValue());
if (wxGetApp().preset_bundle != nullptr && wxGetApp().plater() != nullptr) {
const size_t num_physical = wxGetApp().preset_bundle->filament_presets.size();
// FullSpectrum: record the toggle as the user's authoritative decision for
// the current count, suppressing any future dialog at this same count.
// Adding more filaments later misses the cache and re-prompts as expected.
wxGetApp().plater()->set_auto_generated_gradient_decision(num_physical, checkbox->GetValue());
wxGetApp().preset_bundle->update_multi_material_filament_presets();
wxGetApp().plater()->on_filament_count_change(num_physical);
}
}
if (param == "enable_high_low_temp_mixed_printing") {
if (checkbox->GetValue()) {
const wxString warning_title = _L("Bed Temperature Difference Warning");
@@ -1479,6 +1493,9 @@ void PreferencesDialog::create_items()
auto item_auto_flush = create_item_combobox(_L("Auto flush after changing..."), _L("Auto calculate flushing volumes when selected values changed"), "auto_calculate_flush", FlushOptionLabels, FlushOptionValues);
g_sizer->Add(item_auto_flush);
auto item_auto_generate_gradients = create_item_checkbox(_L("Mixed filaments: Auto-generate gradients."), _L("If enabled, OrcaSlicer automatically creates gradient mixed filaments from physical filament pairs."), "auto_generate_gradients");
g_sizer->Add(item_auto_generate_gradients);
auto item_auto_arrange = create_item_checkbox(_L("Auto arrange plate after cloning"), "", "auto_arrange");
g_sizer->Add(item_auto_arrange);
+24
View File
@@ -2318,6 +2318,7 @@ void TabPrint::build()
auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height");
optgroup->append_single_option_line("layer_height","quality_settings_layer_height");
optgroup->append_single_option_line("initial_layer_print_height","quality_settings_layer_height");
optgroup->append_single_option_line("mixed_filament_gradient_mode");
optgroup = page->new_optgroup(L("Line width"), L"param_line_width");
optgroup->append_single_option_line("line_width","quality_settings_line_width");
@@ -2469,6 +2470,13 @@ void TabPrint::build()
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
if (m_type >= Preset::TYPE_COUNT) {
// Per-object / per-model only: infill filament override
optgroup->append_single_option_line("enable_infill_filament_override");
optgroup->append_single_option_line("infill_filament_use_base_first_layers");
optgroup->append_single_option_line("infill_filament_use_base_last_layers");
optgroup->append_single_option_line("sparse_infill_filament", "multimaterial_settings_filament_for_features#infill");
}
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
optgroup->append_single_option_line("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag");
@@ -2663,6 +2671,7 @@ void TabPrint::build()
optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall");
optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers");
optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower");
optgroup->append_single_option_line("local_z_wipe_tower_purge_lines", "multimaterial_settings_prime_tower");
optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features");
optgroup->append_single_option_line("wall_filament", "multimaterial_settings_filament_for_features#walls");
@@ -2726,6 +2735,21 @@ void TabPrint::build()
optgroup->append_single_option_line("timelapse_type", "others_settings_special_mode#timelapse");
optgroup->append_single_option_line("enable_wrapping_detection");
// Mixed Filaments / Dithering settings
optgroup = page->new_optgroup(L("Dithering"));
optgroup->append_single_option_line("mixed_filament_height_lower_bound");
optgroup->append_single_option_line("mixed_filament_height_upper_bound");
optgroup->append_single_option_line("mixed_filament_advanced_dithering");
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");
optgroup->append_single_option_line("dithering_step_painted_zones_only");
// Local-Z subgroup (gated by dithering_local_z_mode)
optgroup->append_single_option_line("dithering_local_z_mode");
optgroup->append_single_option_line("dithering_local_z_whole_objects");
optgroup->append_single_option_line("dithering_local_z_direct_multicolor");
optgroup = page->new_optgroup(L("Fuzzy Skin"), L"fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin", "others_settings_fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-mode");
+108 -19
View File
@@ -199,24 +199,71 @@ std::string RammingPanel::get_parameters()
static const float g_min_flush_multiplier = 0.f;
static const float g_max_flush_multiplier = 3.f;
// Extract the num_phys×num_phys top-left block from a flat total×total matrix.
// When there are no mixed filaments (total == num_phys) the function is a no-op
// and returns the input unchanged.
static std::vector<double> extract_physical_sub_matrix(
const std::vector<double>& full, size_t total, size_t num_phys)
{
if (num_phys >= total || total == 0)
return full;
std::vector<double> phys(num_phys * num_phys, 0.0);
for (size_t row = 0; row < num_phys; ++row)
for (size_t col = 0; col < num_phys; ++col)
phys[row * num_phys + col] = full[row * total + col];
return phys;
}
// Write the edited num_phys×num_phys sub-matrix back into a copy of
// original_full (total×total), preserving the mixed-slot rows and columns.
static std::vector<double> expand_physical_to_full_matrix(
const std::vector<double>& phys, const std::vector<double>& original_full,
size_t total, size_t num_phys)
{
if (num_phys >= total || total == 0)
return phys;
std::vector<double> result(original_full); // preserve mixed rows/cols
for (size_t row = 0; row < num_phys; ++row)
for (size_t col = 0; col < num_phys; ++col)
result[row * total + col] = phys[row * num_phys + col];
return result;
}
bool is_flush_config_modified()
{
const auto &project_config = wxGetApp().preset_bundle->project_config;
const std::vector<double> &config_matrix = (project_config.option<ConfigOptionFloats>("flush_volumes_matrix"))->values;
const std::vector<double> &config_multiplier = (project_config.option<ConfigOptionFloats>("flush_multiplier"))->values;
// Physical filament count (excludes mixed virtual slots).
const size_t num_phys = static_cast<size_t>(wxGetApp().filaments_cnt());
const size_t nozzle_num = config_multiplier.size();
// Total filament count stored per nozzle block.
const size_t total = (nozzle_num > 0 && !config_matrix.empty())
? static_cast<size_t>(std::round(std::sqrt(config_matrix.size() / nozzle_num)))
: num_phys;
bool has_modify = false;
for (int i = 0; i < config_multiplier.size(); i++) {
for (int i = 0; i < (int)nozzle_num; i++) {
if (config_multiplier[i] != 1) {
has_modify = true;
break;
}
// Extract the per-nozzle block from the flat full matrix.
std::vector<double> nozzle_full(config_matrix.begin() + i * (int)(total * total),
config_matrix.begin() + (i + 1) * (int)(total * total));
// Only compare the physical sub-matrix; mixed-slot rows/cols are computed.
const std::vector<double> phys_stored = extract_physical_sub_matrix(nozzle_full, total, num_phys);
std::vector<std::vector<double>> default_matrix = WipingDialog::CalcFlushingVolumes(i);
int len = default_matrix.size();
for (int m = 0; m < len; m++) {
for (int n = 0; n < len; n++) {
int idx = i * len * len + m * len + n;
if (config_matrix[idx] != default_matrix[m][n] * config_multiplier[i]) {
// CalcFlushingVolumes also spans total×total; take physical sub-matrix.
int def_total = (int)default_matrix.size();
for (int m = 0; m < (int)num_phys; m++) {
for (int n = 0; n < (int)num_phys; n++) {
double def_val = (m < def_total && n < (int)default_matrix[m].size())
? default_matrix[m][n] * config_multiplier[i]
: 0.0;
if (phys_stored[m * num_phys + n] != def_val) {
has_modify = true;
break;
}
@@ -265,24 +312,46 @@ wxString WipingDialog::BuildTableObjStr()
auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
auto nozzle_flush_dataset = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->values;
std::vector<std::vector<double>> flush_matrixs;
// Physical filament count — the editor only shows the P×P physical block.
const size_t num_phys = static_cast<size_t>(wxGetApp().filaments_cnt());
const size_t total = (num_phys > 0 && !filament_colors.empty())
? filament_colors.size()
: num_phys;
// Per-nozzle full matrices (total×total), stored for expand-on-save.
std::vector<std::vector<double>> full_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
flush_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num));
full_matrixs.emplace_back(get_flush_volumes_matrix(raw_matrix_data, idx, nozzle_num));
}
flush_multiplier.resize(nozzle_num, 1);
std::vector<std::vector<float>> default_matrixs;
// Physical sub-matrices sent to the web editor (num_phys×num_phys).
std::vector<std::vector<double>> flush_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
default_matrixs.emplace_back(MatrixFlatten(CalcFlushingVolumes(idx)));
flush_matrixs.emplace_back(extract_physical_sub_matrix(full_matrixs[idx], total, num_phys));
}
m_raw_matrixs = flush_matrixs;
// Default matrices for the auto-calc button — physical sub-matrix only.
std::vector<std::vector<float>> default_matrixs;
for (int idx = 0; idx < nozzle_num; ++idx) {
std::vector<float> def_flat = MatrixFlatten(CalcFlushingVolumes(idx));
std::vector<double> def_d(def_flat.begin(), def_flat.end());
std::vector<double> def_phys = extract_physical_sub_matrix(def_d, total, num_phys);
default_matrixs.emplace_back(def_phys.begin(), def_phys.end());
}
// Store full matrices so storeData can expand back when saving.
m_raw_matrixs = full_matrixs;
m_flush_multipliers = flush_multiplier;
// Only send physical filament colours to the editor.
std::vector<std::string> phys_colors(filament_colors.begin(),
filament_colors.begin() + std::min(num_phys, filament_colors.size()));
json obj;
obj["flush_multiplier"] = flush_multiplier;
obj["extruder_num"] = nozzle_num;
obj["filament_colors"] = filament_colors;
obj["filament_colors"] = phys_colors;
obj["flush_volume_matrixs"] = json::array();
obj["min_flush_volumes"] = json::array();
obj["max_flush_volumes"] = json::array();
@@ -299,8 +368,12 @@ wxString WipingDialog::BuildTableObjStr()
}
for (int idx = 0; idx < nozzle_num; ++idx) {
// min_flush_volumes is indexed by physical slot; slice off at num_phys.
const std::vector<int> &min_flush_volumes = get_min_flush_volumes(full_config, idx);
int min_flush_from_nozzle_volume = *min_element(min_flush_volumes.begin(), min_flush_volumes.end());
int min_flush_from_nozzle_volume = min_flush_volumes.empty()
? 0
: *min_element(min_flush_volumes.begin(),
min_flush_volumes.begin() + std::min(num_phys, min_flush_volumes.size()));
GenericFlushPredictor pd(nozzle_flush_dataset[idx]);
int min_flush_from_flush_data = pd.get_min_flush_volume();
obj["min_flush_volumes"].push_back(std::min(min_flush_from_flush_data,min_flush_from_nozzle_volume));
@@ -468,26 +541,42 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
}
else if (j["msg"].get<std::string>() == "storeData") {
int extruder_num = j["number_of_extruders"].get<int>();
std::vector<std::vector<double>> store_matrixs;
// The web editor works on the physical sub-matrix (P×P).
std::vector<std::vector<double>> phys_matrixs;
for (auto iter = j["raw_matrix"].begin(); iter != j["raw_matrix"].end(); ++iter) {
store_matrixs.emplace_back((*iter).get<std::vector<double>>());
phys_matrixs.emplace_back((*iter).get<std::vector<double>>());
}
std::vector<double>store_multipliers = j["flush_multiplier"].get<std::vector<double>>();
{// limit all matrix value before write to gcode, the limitation is depends on the multipliers
size_t cols_temp_matrix = 0;
if (!store_matrixs.empty()) { cols_temp_matrix = store_matrixs[0].size(); }
if (store_multipliers.size() == store_matrixs.size() && cols_temp_matrix>0) // nuzzles==nuzzles
if (!phys_matrixs.empty()) { cols_temp_matrix = phys_matrixs[0].size(); }
if (store_multipliers.size() == phys_matrixs.size() && cols_temp_matrix>0) // nuzzles==nuzzles
{
for (size_t idx = 0; idx < store_multipliers.size(); ++idx) {
double m_max_flush_volume_t = (double)m_max_flush_volume, m_store_multipliers=store_multipliers[idx];
std::transform(store_matrixs[idx].begin(), store_matrixs[idx].end(),
store_matrixs[idx].begin(),
std::transform(phys_matrixs[idx].begin(), phys_matrixs[idx].end(),
phys_matrixs[idx].begin(),
[m_max_flush_volume_t, m_store_multipliers](double inputx) {
return std::clamp(inputx, 0.0, m_max_flush_volume_t / m_store_multipliers);
});
}
}
}
// Expand physical sub-matrices back to full total×total,
// preserving the mixed-slot rows/cols from the snapshot taken
// in BuildTableObjStr.
const size_t num_phys = static_cast<size_t>(wxGetApp().filaments_cnt());
std::vector<std::vector<double>> store_matrixs;
for (size_t idx = 0; idx < phys_matrixs.size(); ++idx) {
if (idx < m_raw_matrixs.size() && !m_raw_matrixs[idx].empty()) {
const size_t total = static_cast<size_t>(
std::round(std::sqrt(static_cast<double>(m_raw_matrixs[idx].size()))));
store_matrixs.emplace_back(
expand_physical_to_full_matrix(phys_matrixs[idx], m_raw_matrixs[idx], total, num_phys));
} else {
store_matrixs.emplace_back(phys_matrixs[idx]);
}
}
this->StoreFlushData(extruder_num, store_matrixs, store_multipliers);
m_submit_flag = true;
this->Close();
+2 -1
View File
@@ -1,5 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_data.cpp
test_data.hpp
@@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests
test_flow.cpp
test_gcode.cpp
test_gcodewriter.cpp
test_mixed_filament_e2e.cpp
test_model.cpp
test_print.cpp
test_printgcode.cpp
+218
View File
@@ -0,0 +1,218 @@
// Tests for mixed-filament project-config round-trip persistence.
//
// The full-3MF (store_bbs_3mf / load_bbs_3mf) round-trip requires a heavy
// PlateDataPtrs + Model + PresetBundle setup that is not yet scaffolded in
// fff_print tests. We therefore exercise the *persistence layer* directly:
//
// MixedFilamentManager::serialize_custom_entries()
// → stored in project_config["mixed_filament_definitions"]
// → PresetBundle::sync_mixed_filaments_to_config() (mirrors store path)
// → PresetBundle::sync_mixed_filaments_from_config() (mirrors load path)
//
// This is the code path that bbs_3mf.cpp invokes when writing/reading the
// project config block, so a regression here would break 3MF persistence.
//
// TODO: full-pipeline E2E slice test (T0/T1 in G-code, dithering_local_z_mode
// sublayer assertions) requires the full Print::process() scaffolding;
// defer to a follow-up once a minimal PrintObject fixture exists.
#include <catch2/catch_all.hpp>
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/PresetBundle.hpp"
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
using namespace Slic3r;
// ---------------------------------------------------------------------------
// Helper: build a two-filament PresetBundle with colours set.
// ---------------------------------------------------------------------------
namespace {
static PresetBundle make_bundle_2(const std::string &col_a = "#FF0000",
const std::string &col_b = "#0000FF")
{
PresetBundle bundle;
bundle.filament_presets = {"Default Filament", "Default Filament"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {col_a, col_b};
return bundle;
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Test 1 — plain auto-generated round-trip
// Build bundle → sync to config string → reload in fresh bundle → compare
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: auto-generated entries survive serialize/load cycle",
"[MixedFilamentRoundTrip]")
{
PresetBundle origin = make_bundle_2();
origin.sync_mixed_filaments_from_config();
const auto &orig_mixed = origin.mixed_filaments.mixed_filaments();
REQUIRE(!orig_mixed.empty());
// Capture the stable_id and component IDs of the first enabled entry.
const MixedFilament *first = nullptr;
for (const auto &mf : orig_mixed) {
if (mf.enabled && !mf.deleted) { first = &mf; break; }
}
REQUIRE(first != nullptr);
const uint64_t orig_stable_id = first->stable_id;
const unsigned int orig_component_a = first->component_a;
const unsigned int orig_component_b = first->component_b;
// Sync to config (mirrors what store_bbs_3mf does).
origin.sync_mixed_filaments_to_config();
const std::string serialized = origin.project_config.opt_string("mixed_filament_definitions");
// Auto-generated entries are NOT stored in the custom definitions string —
// they are rebuilt by auto_generate() on load. The string will be empty
// unless a custom entry was added. We just verify the round-trip doesn't
// drop auto-generated rows on re-sync.
// Reload into a fresh bundle with the same colours.
PresetBundle loaded = make_bundle_2();
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = serialized;
loaded.sync_mixed_filaments_from_config();
const auto &load_mixed = loaded.mixed_filaments.mixed_filaments();
REQUIRE(load_mixed.size() == orig_mixed.size());
// Find the matching entry by component pair.
const MixedFilament *reloaded = nullptr;
for (const auto &mf : load_mixed) {
if (mf.component_a == orig_component_a && mf.component_b == orig_component_b) {
reloaded = &mf;
break;
}
}
REQUIRE(reloaded != nullptr);
CHECK(reloaded->stable_id == orig_stable_id);
}
// ---------------------------------------------------------------------------
// Test 2 — custom entry round-trip
// 2 physical + 1 custom mixed entry: ratio_a, ratio_b, mix_b_percent, stable_id
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: custom entry count, components, ratio, stable_id",
"[MixedFilamentRoundTrip]")
{
const std::vector<std::string> colors = {"#FF0000", "#00FF00"};
MixedFilamentManager mgr;
// Add two custom entries to exercise the multi-entry path.
mgr.add_custom_filament(1, 2, 25, colors);
mgr.add_custom_filament(1, 2, 75, colors);
const auto &entries = mgr.mixed_filaments();
REQUIRE(entries.size() == 2);
const uint64_t stable_id_0 = entries[0].stable_id;
const uint64_t stable_id_1 = entries[1].stable_id;
CHECK(stable_id_0 != stable_id_1);
// Serialize and reload.
const std::string serialized = mgr.serialize_custom_entries();
REQUIRE(!serialized.empty());
MixedFilamentManager loaded;
loaded.load_custom_entries(serialized, colors);
const auto &reloaded = loaded.mixed_filaments();
REQUIRE(reloaded.size() == 2);
// Order must be preserved.
CHECK(reloaded[0].component_a == 1);
CHECK(reloaded[0].component_b == 2);
CHECK(reloaded[0].mix_b_percent == 25);
CHECK(reloaded[0].stable_id == stable_id_0);
CHECK(reloaded[1].component_a == 1);
CHECK(reloaded[1].component_b == 2);
CHECK(reloaded[1].mix_b_percent == 75);
CHECK(reloaded[1].stable_id == stable_id_1);
}
// ---------------------------------------------------------------------------
// Test 3 — PresetBundle project_config string path (mirrors 3MF store+load)
// Verify that sync_mixed_filaments_to_config + sync_mixed_filaments_from_config
// preserves a custom entry end-to-end through the project_config string.
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: PresetBundle project_config string path",
"[MixedFilamentRoundTrip]")
{
PresetBundle origin = make_bundle_2("#FFFF00", "#FF00FF");
origin.sync_mixed_filaments_from_config();
// Add a custom entry on top of auto-generated ones.
const auto &colors = origin.project_config.option<ConfigOptionStrings>("filament_colour")->values;
origin.mixed_filaments.add_custom_filament(1, 2, 40, colors);
const size_t num_entries_before = origin.mixed_filaments.mixed_filaments().size();
// Count custom entries in origin.
size_t custom_count_before = 0;
uint64_t custom_stable_id = 0;
for (const auto &mf : origin.mixed_filaments.mixed_filaments()) {
if (mf.custom && mf.enabled && !mf.deleted) {
++custom_count_before;
custom_stable_id = mf.stable_id;
}
}
REQUIRE(custom_count_before == 1);
// Sync to config string — mirrors bbs_3mf store path.
origin.sync_mixed_filaments_to_config();
const std::string defs = origin.project_config.opt_string("mixed_filament_definitions");
REQUIRE(!defs.empty());
// Reload — mirrors bbs_3mf load path.
PresetBundle loaded = make_bundle_2("#FFFF00", "#FF00FF");
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = defs;
loaded.sync_mixed_filaments_from_config();
const auto &reloaded = loaded.mixed_filaments.mixed_filaments();
REQUIRE(reloaded.size() == num_entries_before);
// Custom entry must survive with its stable_id intact.
size_t custom_count_after = 0;
uint64_t reloaded_stable_id = 0;
for (const auto &mf : reloaded) {
if (mf.custom && mf.enabled && !mf.deleted) {
++custom_count_after;
reloaded_stable_id = mf.stable_id;
}
}
CHECK(custom_count_after == custom_count_before);
CHECK(reloaded_stable_id == custom_stable_id);
}
// ---------------------------------------------------------------------------
// Test 4 — total_filaments counts physical + mixed correctly after round-trip
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: total_filaments count is stable after reload",
"[MixedFilamentRoundTrip]")
{
PresetBundle origin = make_bundle_2();
origin.sync_mixed_filaments_from_config();
const size_t num_physical = origin.filament_presets.size();
const size_t total_before = origin.mixed_filaments.total_filaments(num_physical);
// For 2 physical filaments C(2,2)=1 virtual → total must be 3.
REQUIRE(total_before == 3u);
origin.sync_mixed_filaments_to_config();
const std::string defs = origin.project_config.opt_string("mixed_filament_definitions");
PresetBundle loaded = make_bundle_2();
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = defs;
loaded.sync_mixed_filaments_from_config();
const size_t total_after = loaded.mixed_filaments.total_filaments(loaded.filament_presets.size());
CHECK(total_after == total_before);
}
+2
View File
@@ -16,6 +16,8 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_mixed_filament.cpp
test_review_fixes.cpp
test_stl.cpp
test_meshboolean.cpp
test_marchingsquares.cpp
File diff suppressed because it is too large Load Diff
+132
View File
@@ -0,0 +1,132 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Slicing.hpp"
#include <string>
#include <vector>
using namespace Slic3r;
// Finding 1 — the helper computing max supported filament ID after a 3MF
// project-config load must include enabled mixed rows.
TEST_CASE("[review-fixes] mixed-aware max filament id", "[review-fixes]")
{
MixedFilamentManager mgr;
const size_t physical_count = 3;
const std::vector<std::string> colors = {"#FF0000", "#00FF00", "#0000FF"};
// No mixed rows: max == physical.
REQUIRE(mgr.total_filaments(physical_count) == physical_count);
// Add an enabled mixed row spanning physical 1 + 2 (custom + enabled by default).
mgr.add_custom_filament(1u, 2u, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
REQUIRE(mgr.total_filaments(physical_count) == physical_count + 1);
// A disabled row does not contribute.
mgr.add_custom_filament(1u, 2u, 25, colors);
REQUIRE(mgr.mixed_filaments().size() == 2);
mgr.mixed_filaments().back().enabled = false;
REQUIRE(mgr.total_filaments(physical_count) == physical_count + 1);
// A deleted row also does not contribute (enabled_count() filters deleted).
mgr.add_custom_filament(2u, 3u, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 3);
mgr.mixed_filaments().back().deleted = true;
mgr.mixed_filaments().back().enabled = false;
REQUIRE(mgr.total_filaments(physical_count) == physical_count + 1);
}
// Finding 2 — passing nullptr for the print object must keep the legacy
// behaviour: no mixed gradient or dithering ranges are applied, and the
// profile is still seeded from the slicing parameters.
TEST_CASE("[review-fixes] update_layer_height_profile passthrough without print object",
"[review-fixes]")
{
Model model;
ModelObject *mo = model.add_object();
REQUIRE(mo != nullptr);
SlicingParameters sp;
sp.layer_height = 0.2;
sp.first_object_layer_height = 0.2;
sp.object_print_z_min = 0.0;
sp.object_print_z_uncompensated_max = 10.0;
std::vector<coordf_t> profile;
const bool updated = PrintObject::update_layer_height_profile(*mo, sp, profile, nullptr);
REQUIRE(updated);
REQUIRE(!profile.empty());
// Strong check: the nullptr branch must be a passthrough to
// layer_height_profile_from_ranges with the model_object's own
// layer_config_ranges — i.e. no mixed-gradient or dithering overrides
// were applied. Computing the expected profile via the same call the
// function performs internally lets a regression that silently sneaks
// those overrides into the nullptr path fail this test.
const std::vector<coordf_t> expected =
Slic3r::layer_height_profile_from_ranges(sp, mo->layer_config_ranges);
REQUIRE(profile == expected);
}
// Finding 4 — ToolOrdering used to push raw 1-based virtual mixed-filament IDs
// directly onto layer_tools.extruders, which then got decremented as if they
// were physical extruder IDs. The fix routes those IDs through the
// MixedFilamentManager so that virtual IDs collapse to a real physical extruder
// while physical IDs pass through unchanged.
TEST_CASE("[review-fixes] resolve_mixed_1based passthrough on physical id",
"[review-fixes]")
{
MixedFilamentManager mgr;
const std::vector<std::string> colors = {"#FF0000", "#00FF00"};
// Add a custom mixed row spanning physical 1 + 2.
mgr.add_custom_filament(1u, 2u, 50, colors);
REQUIRE(mgr.mixed_filaments().size() == 1);
const size_t num_physical = colors.size();
// Physical id (1) must passthrough.
REQUIRE(mgr.resolve(1u, num_physical, /*layer_index=*/0, /*print_z=*/0.f, /*layer_height=*/0.2f) == 1u);
REQUIRE(mgr.resolve(2u, num_physical, /*layer_index=*/0, /*print_z=*/0.f, /*layer_height=*/0.2f) == 2u);
// Virtual id (3 = 2 physical + 1 mixed) must resolve to 1 or 2 depending on cadence.
const unsigned int resolved = mgr.resolve(3u, num_physical, 0, 0.f, 0.2f);
REQUIRE((resolved == 1u || resolved == 2u));
}
TEST_CASE("[review-fixes] mixed/dithering option keys exist in PrintConfig",
"[review-fixes]")
{
// Sanity guard for finding 6: any key the porter wires into invalidation
// must resolve in the config. Build a default-populated DynamicPrintConfig
// that contains every print_config_def key, then probe the option lookup.
static const std::vector<std::string> keys = {
"mixed_filament_definitions",
"mixed_filament_gradient_mode",
"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",
"dithering_z_step_size",
"dithering_local_z_mode",
"dithering_local_z_whole_objects",
"dithering_local_z_direct_multicolor",
"dithering_step_painted_zones_only",
};
std::unique_ptr<DynamicPrintConfig> cfg(DynamicPrintConfig::new_from_defaults_keys(keys));
REQUIRE(cfg != nullptr);
for (const std::string &k : keys) {
INFO("missing config key: " << k);
REQUIRE(cfg->option(k) != nullptr);
// And the canonical print_config_def must know about it.
REQUIRE(print_config_def.get(k) != nullptr);
}
}