mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 21:42:43 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa75b3fa0 | ||
|
|
42b506ad85 | ||
|
|
84c7186674 | ||
|
|
9670da7ddd | ||
|
|
c858360ee6 | ||
|
|
1ecedd3187 | ||
|
|
0346c4931a | ||
|
|
227bdb77da | ||
|
|
2a8782b8f6 | ||
|
|
c0dfe50bc5 | ||
|
|
0e0e34c8b4 |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,20 +394,31 @@ 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;
|
||||
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);
|
||||
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); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1818
-10
File diff suppressed because it is too large
Load Diff
+34
-3
@@ -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,6 +406,16 @@ private:
|
||||
void check_placeholder_parser_failed();
|
||||
size_t get_extruder_id(unsigned int filament_id) const;
|
||||
|
||||
// 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 Point &pos) { m_last_pos = Point3(pos, 0); m_last_pos_defined = true; }
|
||||
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; }
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <libslic3r.h>
|
||||
@@ -36,6 +37,137 @@ 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;
|
||||
}
|
||||
|
||||
void append_unique_preserve_order(std::vector<unsigned int> &dst, unsigned int value)
|
||||
{
|
||||
if (std::find(dst.begin(), dst.end(), value) == dst.end())
|
||||
dst.emplace_back(value);
|
||||
}
|
||||
|
||||
bool internal_solid_infill_uses_sparse_filament(const PrintRegion ®ion, 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 ®ion)
|
||||
{
|
||||
const PrintRegionConfig &config = region.config();
|
||||
|
||||
// Keep legacy "Filament for Features" behavior: an explicit sparse infill
|
||||
// filament choice (different from wall filament) is an override even if the
|
||||
// dedicated toggle is missing or false in the loaded config.
|
||||
const bool explicit_sparse_override =
|
||||
config.sparse_infill_filament.value > 0 &&
|
||||
config.wall_filament.value > 0 &&
|
||||
config.sparse_infill_filament.value != config.wall_filament.value;
|
||||
|
||||
if (!config.enable_infill_filament_override.value && !explicit_sparse_override)
|
||||
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 ®ion)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
void remove_duplicates_preserve_order(std::vector<unsigned int> &values)
|
||||
{
|
||||
std::vector<unsigned int> ordered;
|
||||
ordered.reserve(values.size());
|
||||
for (unsigned int value : values)
|
||||
append_unique_preserve_order(ordered, value);
|
||||
values = std::move(ordered);
|
||||
}
|
||||
|
||||
} // 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 +211,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 ®ion) 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 ®ion) 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 ®ion) 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 +263,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;
|
||||
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 ®ion) const
|
||||
{
|
||||
return sparse_infill_filament_id_1based_impl(*this, region);
|
||||
}
|
||||
|
||||
unsigned int LayerTools::infill_filament_id_1based(const PrintRegion ®ion) 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 ®ion) const
|
||||
{
|
||||
return use_base_infill_filament_impl(*this, region);
|
||||
}
|
||||
|
||||
static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height)
|
||||
@@ -179,6 +347,7 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector<unsigned int>& too
|
||||
// Reorder the extruders of first layer
|
||||
{
|
||||
LayerTools& lt = m_layer_tools[0];
|
||||
if (!lt.preserve_extruder_order) {
|
||||
std::vector<unsigned int> layer0_extruders = lt.extruders;
|
||||
lt.extruders.clear();
|
||||
for (unsigned int extruder_id : tool_order_layer0) {
|
||||
@@ -202,6 +371,7 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector<unsigned int>& too
|
||||
lt.extruders.push_back(tool_order_layer0[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int last_extruder_id = m_layer_tools[0].extruders.back();
|
||||
for (int i = 1; i < m_layer_tools.size(); i++) {
|
||||
@@ -215,6 +385,10 @@ void ToolOrdering::handle_dontcare_extruder(const std::vector<unsigned int>& too
|
||||
if (lt.extruders.front() == 0)
|
||||
// Pop the "don't care" extruder, the "don't care" region will be merged with the next one.
|
||||
lt.extruders.erase(lt.extruders.begin());
|
||||
if (lt.preserve_extruder_order) {
|
||||
last_extruder_id = lt.extruders.back();
|
||||
continue;
|
||||
}
|
||||
// Reorder the extruders to start with the last one.
|
||||
for (size_t i = 1; i < lt.extruders.size(); ++i)
|
||||
if (lt.extruders[i] == last_extruder_id) {
|
||||
@@ -269,6 +443,10 @@ void ToolOrdering::handle_dontcare_extruder(unsigned int last_extruder_id)
|
||||
if (lt.extruders.front() == 0)
|
||||
// Pop the "don't care" extruder, the "don't care" region will be merged with the next one.
|
||||
lt.extruders.erase(lt.extruders.begin());
|
||||
if (lt.preserve_extruder_order) {
|
||||
last_extruder_id = lt.extruders.back();
|
||||
continue;
|
||||
}
|
||||
// Reorder the extruders to start with the last one.
|
||||
for (size_t i = 1; i < lt.extruders.size(); ++ i)
|
||||
if (lt.extruders[i] == last_extruder_id) {
|
||||
@@ -384,6 +562,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 +611,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 +667,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 +859,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 < : 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 +881,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 +908,40 @@ 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);
|
||||
const unsigned int configured_wall = (extruder_override == 0)
|
||||
? region.config().wall_filament.value
|
||||
: extruder_override;
|
||||
unsigned int wall_ext = resolve_mixed(configured_wall,
|
||||
layerCount,
|
||||
float(layer->print_z),
|
||||
float(layer->height));
|
||||
const unsigned int grouped_id =
|
||||
grouped_manual_pattern_mixed_filament_id_for_layer(layer_tools, configured_wall);
|
||||
if (grouped_id != 0) {
|
||||
const std::vector<unsigned int> ordered =
|
||||
m_mixed_mgr->ordered_perimeter_extruders(grouped_id,
|
||||
m_num_physical,
|
||||
layerCount,
|
||||
float(layer->print_z),
|
||||
float(layer->height));
|
||||
if (!ordered.empty()) {
|
||||
if (ordered.size() >= 2)
|
||||
layer_tools.preserve_extruder_order = true;
|
||||
for (unsigned int extruder_id : ordered) {
|
||||
layer_tools.extruders.emplace_back(extruder_id);
|
||||
if (layerCount == 0 &&
|
||||
std::find(firstLayerExtruders.begin(), firstLayerExtruders.end(), int(extruder_id)) == firstLayerExtruders.end())
|
||||
firstLayerExtruders.emplace_back(int(extruder_id));
|
||||
}
|
||||
} else {
|
||||
layer_tools.extruders.emplace_back(wall_ext);
|
||||
if (layerCount == 0)
|
||||
firstLayerExtruders.emplace_back(wall_ext);
|
||||
}
|
||||
} else {
|
||||
layer_tools.extruders.emplace_back(wall_ext);
|
||||
if (layerCount == 0)
|
||||
firstLayerExtruders.emplace_back(wall_ext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,11 +969,15 @@ 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);
|
||||
layer_tools.extruders.emplace_back(layer_tools.solid_infill_filament(region) + 1);
|
||||
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);
|
||||
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;
|
||||
@@ -730,18 +991,23 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
// Collect the support extruders.
|
||||
for (auto support_layer : object.support_layers()) {
|
||||
LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z);
|
||||
layer_tools.layer_height = support_layer->height;
|
||||
ExtrusionRole role = support_layer->support_fills.role();
|
||||
bool has_support = false;
|
||||
bool has_interface = false;
|
||||
for (const ExtrusionEntity *ee : support_layer->support_fills.entities) {
|
||||
ExtrusionRole er = ee->role();
|
||||
if (er == erSupportMaterial || er == erSupportTransition) has_support = true;
|
||||
if (er == erSupportMaterialInterface) has_interface = true;
|
||||
if (has_support && has_interface) break;
|
||||
}
|
||||
unsigned int extruder_support = object.config().support_filament.value;
|
||||
unsigned int extruder_interface = object.config().support_interface_filament.value;
|
||||
bool has_support = role == erMixed || role == erSupportMaterial || role == erSupportTransition;
|
||||
bool has_interface = role == erMixed || role == erSupportMaterialInterface;
|
||||
|
||||
unsigned int extruder_support = resolve_mixed(object.config().support_filament.value,
|
||||
layer_tools.layer_index,
|
||||
float(support_layer->print_z),
|
||||
float(support_layer->height));
|
||||
unsigned int extruder_interface = resolve_mixed(object.config().support_interface_filament.value,
|
||||
layer_tools.layer_index,
|
||||
float(support_layer->print_z),
|
||||
float(support_layer->height));
|
||||
|
||||
if (has_support) {
|
||||
// BP-only fallback: when support_filament is unset and an interface
|
||||
// exists, pick the lowest-flush non-soluble body extruder.
|
||||
if (extruder_support > 0 || !has_interface || extruder_interface == 0 || layer_tools.has_object)
|
||||
layer_tools.extruders.push_back(extruder_support);
|
||||
else {
|
||||
@@ -750,7 +1016,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
std::vector<float> flush_matrix(
|
||||
cast<float>(get_flush_volumes_matrix(object.print()->config().flush_volumes_matrix.values, 0, object.print()->config().nozzle_diameter.values.size())));
|
||||
const unsigned int number_of_extruders = (unsigned int) (sqrt(flush_matrix.size()) + EPSILON);
|
||||
// Extract purging volumes for each extruder pair:
|
||||
std::vector<std::vector<float>> wipe_volumes;
|
||||
for (unsigned int i = 0; i < number_of_extruders; ++i)
|
||||
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders, flush_matrix.begin() + (i + 1) * number_of_extruders));
|
||||
@@ -777,7 +1042,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
}
|
||||
|
||||
for (auto& layer : m_layer_tools) {
|
||||
// Sort and remove duplicates
|
||||
if (layer.preserve_extruder_order)
|
||||
remove_duplicates_preserve_order(layer.extruders);
|
||||
else
|
||||
sort_remove_duplicates(layer.extruders);
|
||||
|
||||
// make sure that there are some tools for each object layer (e.g. tall wiping object will result in empty extruders vector)
|
||||
@@ -1333,6 +1600,9 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
&filament_sequences
|
||||
);
|
||||
|
||||
// TODO(fs-port): for layers with preserve_extruder_order=true the stats
|
||||
// here reflect the optimized sequence; the guarded writeback below keeps
|
||||
// the original ordering. UI-only divergence — see line ~1610 below.
|
||||
auto curr_flush_info = calc_filament_change_info_by_toolorder(print_config, filament_maps, nozzle_flush_mtx, filament_sequences);
|
||||
if (nozzle_nums <= 1)
|
||||
m_stats_by_single_extruder = curr_flush_info;
|
||||
@@ -1374,9 +1644,20 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < filament_sequences.size(); ++i)
|
||||
for (size_t i = 0; i < filament_sequences.size(); ++i) {
|
||||
// FS preserve_extruder_order guard: keep the layer's existing extruder
|
||||
// ordering by skipping writeback of the optimized sequence.
|
||||
//
|
||||
// Note: BP runs the optimizer (and updates stats) BEFORE this writeback
|
||||
// because the optimizer is a free function in ToolOrderUtils.cpp without
|
||||
// LayerTools access. Stats may diverge from g-code reality on
|
||||
// preserve-order layers. See FS ToolOrdering.cpp:1156-1159 for the
|
||||
// upstream behavior that guards earlier in the call chain.
|
||||
if (m_layer_tools[i].preserve_extruder_order)
|
||||
continue;
|
||||
m_layer_tools[i].extruders = std::move(filament_sequences[i]);
|
||||
}
|
||||
}
|
||||
// Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed.
|
||||
void ToolOrdering::mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height)
|
||||
{
|
||||
@@ -1833,5 +2114,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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#define slic3r_ToolOrdering_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../MixedFilament.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -145,14 +146,44 @@ 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 ®ion) 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 ®ion) const;
|
||||
unsigned int infill_filament_id_1based(const PrintRegion ®ion) const;
|
||||
bool use_base_infill_filament(const PrintRegion ®ion) const;
|
||||
|
||||
coordf_t print_z = 0.;
|
||||
bool has_object = false;
|
||||
bool has_support = false;
|
||||
// Zero based extruder IDs, ordered to minimize tool switches.
|
||||
std::vector<unsigned int> extruders;
|
||||
// When set, downstream reorder passes leave this layer's extruder
|
||||
// sequence in place (used by grouped manual mixed-filament patterns).
|
||||
bool preserve_extruder_order = false;
|
||||
// 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 +203,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 +289,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 +305,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 +323,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
if (tool_change != nullptr) {
|
||||
wipe_volume = tool_change->wipe_volume;
|
||||
wipe_area = tool_change->required_depth;
|
||||
}
|
||||
}
|
||||
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()) {
|
||||
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))
|
||||
.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;
|
||||
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;
|
||||
(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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
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)
|
||||
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,6 +2681,14 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
|
||||
for (const WipeTower2::WipeTowerInfo& layer : m_plan)
|
||||
{
|
||||
std::vector<WipeTower::ToolChangeResult> layer_result;
|
||||
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;
|
||||
|
||||
@@ -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));
|
||||
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();
|
||||
}
|
||||
@@ -2436,6 +2728,7 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -0,0 +1,317 @@
|
||||
#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);
|
||||
|
||||
// Build a standardized user-facing mixed filament name.
|
||||
// - Pattern rows: "Pattern <flattened pattern>" (for example "Pattern 1212354").
|
||||
// - Mix rows: "<id>:<pct>% + <id>:<pct>% ..." (for example "1:30% + 2:20% + 3:50%").
|
||||
std::string mixed_filament_standardized_name(const MixedFilament &entry, size_t num_physical);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1829,15 +1829,14 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
|
||||
{
|
||||
const size_t num_layers = segmented_regions.size();
|
||||
std::vector<std::vector<ExPolygons>> segmented_regions_merged(num_layers);
|
||||
segmented_regions_merged.assign(num_layers, std::vector<ExPolygons>(num_facets_states - 1));
|
||||
segmented_regions_merged.assign(num_layers, std::vector<ExPolygons>(num_facets_states));
|
||||
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
|
||||
assert(segmented_regions[layer_idx].size() == num_facets_states);
|
||||
// Zero is skipped because it is the default color of the volume
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
|
||||
for (size_t extruder_id = 0; extruder_id < num_facets_states; ++extruder_id) {
|
||||
throw_on_cancel_callback();
|
||||
if (!segmented_regions[layer_idx][extruder_id].empty()) {
|
||||
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
|
||||
@@ -1849,16 +1848,16 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
|
||||
}
|
||||
}
|
||||
|
||||
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
|
||||
segmented_regions_merged[layer_idx][extruder_id] = std::move(segmented_regions_trimmed);
|
||||
}
|
||||
|
||||
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
|
||||
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
|
||||
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id].empty();
|
||||
append(segmented_regions_merged[layer_idx][extruder_id], top_and_bottom_layers[extruder_id][layer_idx]);
|
||||
|
||||
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
|
||||
if (!was_top_and_bottom_empty)
|
||||
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
|
||||
segmented_regions_merged[layer_idx][extruder_id] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2195,7 +2194,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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
+217
-10
@@ -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();
|
||||
@@ -4095,20 +4154,22 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
|
||||
//BBS: add logic for settings check between different system presets
|
||||
out.erase("different_settings_to_system");
|
||||
|
||||
static const char* keys[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"};
|
||||
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++ i) {
|
||||
std::string key = std::string(keys[i]);
|
||||
const size_t num_total_filaments = this->mixed_filaments.total_filaments(num_filaments);
|
||||
|
||||
static const char* support_keys[] = {"support_filament", "support_interface_filament"};
|
||||
for (size_t i = 0; i < sizeof(support_keys) / sizeof(support_keys[0]); ++ i) {
|
||||
std::string key = std::string(support_keys[i]);
|
||||
auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false));
|
||||
assert(opt != nullptr);
|
||||
opt->value = boost::algorithm::clamp<int>(opt->value, 0, int(num_filaments));
|
||||
}
|
||||
|
||||
static const char* keys_with_default[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament"};
|
||||
for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++ i) {
|
||||
std::string key = std::string(keys_with_default[i]);
|
||||
static const char* feature_keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", "wipe_tower_filament"};
|
||||
for (size_t i = 0; i < sizeof(feature_keys) / sizeof(feature_keys[0]); ++ i) {
|
||||
std::string key = std::string(feature_keys[i]);
|
||||
auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false));
|
||||
assert(opt != nullptr);
|
||||
if(opt->value < 0 || opt->value > int(num_filaments))
|
||||
if (opt->value < 0 || opt->value > int(num_total_filaments))
|
||||
opt->value = 0;
|
||||
}
|
||||
out.option<ConfigOptionString >("print_settings_id", true)->value = this->prints.get_selected_preset_name();
|
||||
@@ -4557,6 +4618,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 +5181,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 +5192,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 +5209,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 +5261,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)
|
||||
|
||||
@@ -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
@@ -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.to_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.to_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.to_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
@@ -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;
|
||||
|
||||
@@ -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,60 @@ 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_pointillism_pixel_size", true);
|
||||
new_full_config.option("mixed_filament_pointillism_line_gap", true);
|
||||
new_full_config.option("mixed_filament_component_bias_enabled", true);
|
||||
new_full_config.option("mixed_filament_surface_indentation", true);
|
||||
new_full_config.option("mixed_filament_region_collapse", true);
|
||||
new_full_config.option("mixed_filament_definitions", true);
|
||||
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_pointillism_pixel_size", true);
|
||||
m_config.option("mixed_filament_pointillism_line_gap", true);
|
||||
m_config.option("mixed_filament_component_bias_enabled", true);
|
||||
m_config.option("mixed_filament_surface_indentation", true);
|
||||
m_config.option("mixed_filament_region_collapse", true);
|
||||
m_config.option("mixed_filament_definitions", true);
|
||||
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_pointillism_pixel_size", true);
|
||||
m_default_object_config.option("mixed_filament_pointillism_line_gap", true);
|
||||
m_default_object_config.option("mixed_filament_component_bias_enabled", true);
|
||||
m_default_object_config.option("mixed_filament_surface_indentation", true);
|
||||
m_default_object_config.option("mixed_filament_region_collapse", true);
|
||||
m_default_object_config.option("mixed_filament_definitions", true);
|
||||
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 +1386,87 @@ 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_pointillism_pixel_size = 0.f;
|
||||
float mixed_pointillism_line_gap = 0.f;
|
||||
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_pointillism_pixel_size"))
|
||||
mixed_pointillism_pixel_size = float(new_full_config.opt_float("mixed_filament_pointillism_pixel_size"));
|
||||
if (new_full_config.has("mixed_filament_pointillism_line_gap"))
|
||||
mixed_pointillism_line_gap = float(new_full_config.opt_float("mixed_filament_pointillism_line_gap"));
|
||||
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_pointillism_pixel_size = std::max(0.f, mixed_pointillism_pixel_size);
|
||||
mixed_pointillism_line_gap = std::max(0.f, mixed_pointillism_line_gap);
|
||||
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)
|
||||
<< ", pointillism_pixel_size=" << mixed_pointillism_pixel_size
|
||||
<< ", pointillism_line_gap=" << mixed_pointillism_line_gap
|
||||
<< ", 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 +1654,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 +1720,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()) {
|
||||
@@ -1675,17 +1861,24 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
std::array<bool, static_cast<size_t>(EnforcerBlockerType::ExtruderMax) + 1> used_facet_states{};
|
||||
for (const ModelVolume *volume : volumes) {
|
||||
const std::vector<bool> &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states;
|
||||
|
||||
assert(volume_used_facet_states.size() == used_facet_states.size());
|
||||
for (size_t state_idx = 0; state_idx < std::min(volume_used_facet_states.size(), used_facet_states.size()); ++state_idx)
|
||||
used_facet_states[state_idx] |= volume_used_facet_states[state_idx];
|
||||
}
|
||||
|
||||
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.
|
||||
auto invalidate = [it_print_object, it_print_object_end, update_apply_status]() {
|
||||
@@ -1702,7 +1895,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 +1920,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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
+579
-16
@@ -992,6 +992,7 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
|
||||
|
||||
void PrintObject::clear_layers()
|
||||
{
|
||||
this->clear_local_z_plan();
|
||||
if (!m_shared_object) {
|
||||
for (Layer *l : m_layers)
|
||||
delete l;
|
||||
@@ -1138,6 +1139,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 +1162,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"
|
||||
@@ -1450,6 +1471,7 @@ bool PrintObject::invalidate_step(PrintObjectStep step)
|
||||
invalidated |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill });
|
||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||
m_slicing_params.valid = false;
|
||||
this->clear_local_z_plan();
|
||||
} else if (step == posSupportMaterial) {
|
||||
invalidated |= this->invalidate_steps({ posSimplifySupportPath });
|
||||
invalidated |= m_print->invalidate_steps({ psSkirtBrim });
|
||||
@@ -1471,6 +1493,7 @@ bool PrintObject::invalidate_all_steps()
|
||||
bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps();
|
||||
// Then reset some of the depending values.
|
||||
m_slicing_params.valid = false;
|
||||
this->clear_local_z_plan();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3555,7 +3578,8 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
|
||||
if (it->first != key_extruder)
|
||||
if (ConfigOption* my_opt = out.option(it->first, false); my_opt != nullptr) {
|
||||
if (one_of(it->first, keys_extruders)) {
|
||||
// "Default" (0) clears explicit override for this scope and lets fallback apply.
|
||||
// "Default" (0) keeps any prior explicit feature override from parent scopes.
|
||||
// This lets object/part base filament be the default unless an explicit feature filament was chosen.
|
||||
int extruder = static_cast<const ConfigOptionInt*>(it->second.get())->value;
|
||||
if (extruder > 0) {
|
||||
my_opt->setInt(extruder);
|
||||
@@ -3565,13 +3589,6 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
|
||||
feature_overrides.solid_infill_filament = true;
|
||||
else if (it->first == "wall_filament")
|
||||
feature_overrides.wall_filament = true;
|
||||
} else {
|
||||
if (it->first == "sparse_infill_filament")
|
||||
feature_overrides.sparse_infill_filament = false;
|
||||
else if (it->first == "solid_infill_filament")
|
||||
feature_overrides.solid_infill_filament = false;
|
||||
else if (it->first == "wall_filament")
|
||||
feature_overrides.wall_filament = false;
|
||||
}
|
||||
} else
|
||||
my_opt->set(it->second.get());
|
||||
@@ -3720,16 +3737,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 +4306,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;
|
||||
}
|
||||
|
||||
|
||||
+3398
-38
File diff suppressed because it is too large
Load Diff
@@ -1691,9 +1691,11 @@ void TriangleSelector::get_seed_fill_contour_recursive(const int facet_idx, cons
|
||||
|
||||
TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
|
||||
// Each original triangle of the mesh is assigned a number encoding its state
|
||||
// or how it is split. Each triangle is encoded by 4 bits (xxyy) or 8 bits (zzzzxxyy):
|
||||
// or how it is split. Each triangle is encoded by 4 bits (xxyy), 8 bits
|
||||
// (zzzzxxyy), or 12 bits (wwwwzzzzxxyy):
|
||||
// leaf triangle: xx = EnforcerBlockerType (Only values 0, 1, and 2. Value 3 is used as an indicator for additional 4 bits.), yy = 0
|
||||
// leaf triangle: xx = 0b11, yy = 0b00, zzzz = EnforcerBlockerType (subtracted by 3)
|
||||
// leaf triangle: xx = 0b11, yy = 0b00, zzzz = EnforcerBlockerType (subtracted by 3) for states 3..16
|
||||
// leaf triangle: xx = 0b11, yy = 0b00, zzzz = 0b1110, wwww = EnforcerBlockerType (subtracted by 17) for states 17..32
|
||||
// non-leaf: xx = special side, yy = number of split sides
|
||||
// These are bitwise appended and formed into one 64-bit integer.
|
||||
|
||||
@@ -1736,13 +1738,25 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
|
||||
data.used_states[n] = true;
|
||||
|
||||
if (n >= 3) {
|
||||
assert(n <= 16);
|
||||
if (n <= 16) {
|
||||
// Store "11" plus 4 bits of (n-3).
|
||||
data.bitstream.insert(data.bitstream.end(), { true, true });
|
||||
n -= 3;
|
||||
assert(n <= int(EnforcerBlockerType::ExtruderMax));
|
||||
|
||||
auto push_nibble = [&data = this->data](int value) {
|
||||
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx)
|
||||
data.bitstream.push_back(n & (uint64_t(0b0001) << bit_idx));
|
||||
data.bitstream.push_back((value & (1 << int(bit_idx))) != 0);
|
||||
};
|
||||
|
||||
// Store "11" plus either one nibble (legacy 3..16) or
|
||||
// an escaped second nibble for 17..32.
|
||||
data.bitstream.insert(data.bitstream.end(), { true, true });
|
||||
if (n <= 16) {
|
||||
push_nibble(n - 3);
|
||||
} else {
|
||||
// 0b1110 marks the extended range 17..32.
|
||||
constexpr int extended_prefix = 0b1110;
|
||||
const int encoded = n - 17;
|
||||
assert(encoded >= 0 && encoded <= 15);
|
||||
push_nibble(extended_prefix);
|
||||
push_nibble(encoded);
|
||||
}
|
||||
} else {
|
||||
// Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams.
|
||||
@@ -1818,8 +1832,19 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||
int num_of_split_sides = code & 0b11;
|
||||
int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1;
|
||||
bool is_split = num_of_children != 0;
|
||||
// Only valid if not is_split. Value of the second nibble was subtracted by 3, so it is added back.
|
||||
auto state = is_split ? EnforcerBlockerType::NONE : EnforcerBlockerType((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2);
|
||||
// Only valid if not is_split.
|
||||
auto decode_leaf_state = [&next_nibble](int leaf_code) -> EnforcerBlockerType {
|
||||
if ((leaf_code & 0b1100) != 0b1100)
|
||||
return EnforcerBlockerType(leaf_code >> 2);
|
||||
|
||||
const int extended = next_nibble();
|
||||
if (extended == 0b1110)
|
||||
return EnforcerBlockerType(next_nibble() + 17);
|
||||
|
||||
// Legacy path (states 3..16, plus historical fallback for 18).
|
||||
return EnforcerBlockerType(extended + 3);
|
||||
};
|
||||
auto state = is_split ? EnforcerBlockerType::NONE : decode_leaf_state(code);
|
||||
|
||||
// BBS
|
||||
if (state == to_delete_filament)
|
||||
@@ -1916,7 +1941,17 @@ void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bi
|
||||
if (const bool is_split = (code & 0b11) != 0; is_split)
|
||||
continue;
|
||||
|
||||
const uint8_t facet_state = (code & 0b1100) == 0b1100 ? read_next_nibble() + 3 : code >> 2;
|
||||
uint8_t facet_state = 0;
|
||||
if ((code & 0b1100) != 0b1100) {
|
||||
facet_state = code >> 2;
|
||||
} else {
|
||||
const uint8_t extended = read_next_nibble();
|
||||
if (extended == 0b1110)
|
||||
facet_state = read_next_nibble() + 17;
|
||||
else
|
||||
facet_state = extended + 3;
|
||||
}
|
||||
|
||||
assert(facet_state < this->used_states.size());
|
||||
if (facet_state >= this->used_states.size())
|
||||
continue;
|
||||
@@ -1946,9 +1981,17 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
||||
auto num_children_or_state = [&next_nibble]() -> int {
|
||||
int code = next_nibble();
|
||||
int num_of_split_sides = code & 0b11;
|
||||
return num_of_split_sides == 0 ?
|
||||
((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) :
|
||||
- num_of_split_sides - 1;
|
||||
if (num_of_split_sides != 0)
|
||||
return - num_of_split_sides - 1;
|
||||
|
||||
if ((code & 0b1100) != 0b1100)
|
||||
return code >> 2;
|
||||
|
||||
const int extended = next_nibble();
|
||||
if (extended == 0b1110)
|
||||
return next_nibble() + 17;
|
||||
|
||||
return extended + 3;
|
||||
};
|
||||
|
||||
int state = num_children_or_state();
|
||||
@@ -2524,4 +2567,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
|
||||
|
||||
@@ -17,7 +17,8 @@ enum class EnforcerBlockerType : int8_t {
|
||||
BLOCKER = 2,
|
||||
// For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN).
|
||||
FUZZY_SKIN = ENFORCER,
|
||||
// Maximum is 15. The value is serialized in TriangleSelector into 6 bits using a 2 bit prefix code.
|
||||
// Extruder states use compact nibble encoding with extended fallback in TriangleSelector serialization.
|
||||
// Values above 16 are supported as long as they fit in EnforcerBlockerType (int8_t).
|
||||
Extruder1 = ENFORCER,
|
||||
Extruder2 = BLOCKER,
|
||||
Extruder3,
|
||||
@@ -34,7 +35,23 @@ enum class EnforcerBlockerType : int8_t {
|
||||
Extruder14,
|
||||
Extruder15,
|
||||
Extruder16,
|
||||
ExtruderMax = Extruder16
|
||||
Extruder17,
|
||||
Extruder18,
|
||||
Extruder19,
|
||||
Extruder20,
|
||||
Extruder21,
|
||||
Extruder22,
|
||||
Extruder23,
|
||||
Extruder24,
|
||||
Extruder25,
|
||||
Extruder26,
|
||||
Extruder27,
|
||||
Extruder28,
|
||||
Extruder29,
|
||||
Extruder30,
|
||||
Extruder31,
|
||||
Extruder32,
|
||||
ExtruderMax = Extruder32
|
||||
};
|
||||
|
||||
// Type alias for the state mapping array to improve code readability
|
||||
@@ -391,6 +408,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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -479,20 +479,43 @@ 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]);
|
||||
// Rule 1:
|
||||
// - support slots stay physical-only
|
||||
// - feature slots (wall/infill/wipe tower) may use mixed virtual IDs
|
||||
// Any out-of-range value is reset to 0.
|
||||
{
|
||||
static const char* support_slot_keys[] = {
|
||||
"support_filament", "support_interface_filament"
|
||||
};
|
||||
static const char* feature_slot_keys[] = {
|
||||
"wall_filament", "sparse_infill_filament", "solid_infill_filament", "wipe_tower_filament"
|
||||
};
|
||||
|
||||
const size_t total = wxGetApp().preset_bundle->total_filament_count();
|
||||
const size_t num_phys = wxGetApp().preset_bundle->filament_presets.size();
|
||||
|
||||
for (auto key : support_slot_keys) {
|
||||
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
|
||||
if (opt != nullptr) {
|
||||
if (opt->getInt() > filament_cnt) {
|
||||
if (!opt)
|
||||
continue;
|
||||
const int val = opt->getInt();
|
||||
const bool out_of_range = val < 0 || val > int(total);
|
||||
const 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(0));
|
||||
apply(config, &new_conf);
|
||||
}
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(new_value));
|
||||
}
|
||||
|
||||
for (auto key : feature_slot_keys) {
|
||||
auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
|
||||
if (!opt)
|
||||
continue;
|
||||
const int val = opt->getInt();
|
||||
if (val < 0 || val > int(total)) {
|
||||
DynamicPrintConfig new_conf = *config;
|
||||
new_conf.set_key_value(key, new ConfigOptionInt(0));
|
||||
apply(config, &new_conf);
|
||||
}
|
||||
}
|
||||
@@ -541,6 +564,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 +1035,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*/)
|
||||
|
||||
@@ -263,6 +263,33 @@ bool BitmapChoiceRenderer::GetValue(wxVariant& value) const
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::vector<wxBitmap*> mixed_aware_extruder_icons(bool thin_icon = false)
|
||||
{
|
||||
std::vector<wxBitmap*> icons = get_extruder_color_icons(thin_icon);
|
||||
|
||||
if (Slic3r::GUI::wxGetApp().plater() == nullptr)
|
||||
return icons;
|
||||
|
||||
const std::vector<std::string> all_colors =
|
||||
Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, true);
|
||||
if (all_colors.size() <= icons.size())
|
||||
return icons;
|
||||
|
||||
const double em = Slic3r::GUI::wxGetApp().em_unit();
|
||||
const int icon_width = int((thin_icon ? 2.0 : 4.4) * em + 0.5);
|
||||
const int icon_height = int(2.0 * em + 0.5);
|
||||
for (size_t idx = icons.size(); idx < all_colors.size(); ++idx) {
|
||||
if (all_colors[idx].empty()) {
|
||||
icons.push_back(nullptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
icons.push_back(get_extruder_color_icon(all_colors[idx], std::to_string(idx + 1), icon_width, icon_height));
|
||||
}
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
bool BitmapChoiceRenderer::Render(wxRect rect, wxDC* dc, int state)
|
||||
{
|
||||
const wxBitmap& icon = m_value.GetBitmap();
|
||||
@@ -301,7 +328,7 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR
|
||||
if (can_create_editor_ctrl && !can_create_editor_ctrl())
|
||||
return nullptr;
|
||||
|
||||
std::vector<wxBitmap*> icons = get_extruder_color_icons();
|
||||
std::vector<wxBitmap*> icons = mixed_aware_extruder_icons(false);
|
||||
if (icons.empty())
|
||||
return nullptr;
|
||||
|
||||
@@ -317,12 +344,14 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR
|
||||
c_editor->Append(_L("default"), *get_default_extruder_color_icon());
|
||||
|
||||
for (size_t i = 0; i < icons.size(); i++)
|
||||
c_editor->Append(wxString::Format("%d", i+1), *icons[i]);
|
||||
c_editor->Append(wxString::Format("%d", i + 1), icons[i] ? *icons[i] : wxNullBitmap);
|
||||
|
||||
if (has_default_extruder && has_default_extruder())
|
||||
c_editor->SetSelection(atoi(data.GetText().c_str()));
|
||||
else
|
||||
c_editor->SetSelection(atoi(data.GetText().c_str()) - 1);
|
||||
int selection = (has_default_extruder && has_default_extruder())
|
||||
? atoi(data.GetText().c_str())
|
||||
: atoi(data.GetText().c_str()) - 1;
|
||||
if (selection < 0 || selection >= int(c_editor->GetCount()))
|
||||
selection = 0;
|
||||
c_editor->SetSelection(selection);
|
||||
|
||||
c_editor->Bind(wxEVT_SET_FOCUS, [c_editor](wxFocusEvent& evt) {
|
||||
#ifdef __WXGTK__
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -35,9 +35,53 @@ static PrinterTechnology printer_technology()
|
||||
return wxGetApp().preset_bundle->printers.get_selected_preset().printer_technology();
|
||||
}
|
||||
|
||||
static int physical_filaments_count()
|
||||
{
|
||||
return std::max(wxGetApp().filaments_cnt(), 0);
|
||||
}
|
||||
|
||||
static int filaments_count()
|
||||
{
|
||||
return wxGetApp().filaments_cnt();
|
||||
if (wxGetApp().preset_bundle == nullptr)
|
||||
return 0;
|
||||
const int physical = physical_filaments_count();
|
||||
const auto &mixed_mgr = wxGetApp().preset_bundle->mixed_filaments;
|
||||
return static_cast<int>(mixed_mgr.total_filaments(size_t(physical)));
|
||||
}
|
||||
|
||||
static std::vector<unsigned int> ui_ordered_filament_ids()
|
||||
{
|
||||
if (wxGetApp().plater() == nullptr)
|
||||
return {};
|
||||
return wxGetApp().plater()->sidebar().get_ui_ordered_filament_ids();
|
||||
}
|
||||
|
||||
static wxString filament_menu_item_name(const int filament_id_1based)
|
||||
{
|
||||
if (filament_id_1based <= 0)
|
||||
return _L("Default");
|
||||
|
||||
if (wxGetApp().preset_bundle == nullptr)
|
||||
return wxString::Format(_L("Filament %d"), filament_id_1based);
|
||||
|
||||
const int physical = physical_filaments_count();
|
||||
if (filament_id_1based <= physical) {
|
||||
const size_t preset_idx = size_t(filament_id_1based - 1);
|
||||
const auto &filament_presets = wxGetApp().preset_bundle->filament_presets;
|
||||
if (preset_idx < filament_presets.size()) {
|
||||
auto preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[preset_idx]);
|
||||
if (preset != nullptr)
|
||||
return from_u8(preset->label(false));
|
||||
}
|
||||
return wxString::Format(_L("Filament %d"), filament_id_1based);
|
||||
}
|
||||
|
||||
const auto &mgr = wxGetApp().preset_bundle->mixed_filaments;
|
||||
const MixedFilament *mixed = mgr.mixed_filament_from_id(unsigned(filament_id_1based), size_t(physical));
|
||||
if (mixed == nullptr)
|
||||
return _L("Mixed Filament");
|
||||
|
||||
return from_u8(mixed_filament_standardized_name(*mixed, size_t(physical)));
|
||||
}
|
||||
|
||||
static bool is_improper_category(const std::string& category, const int filaments_cnt, const bool is_object_settings = true)
|
||||
@@ -1042,8 +1086,9 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
|
||||
menu->Destroy(item_id);
|
||||
}
|
||||
|
||||
const int filaments_cnt = filaments_count();
|
||||
if (filaments_cnt <= 1)
|
||||
// Use UI-ordered filament IDs (physical first, then enabled mixed in UI order)
|
||||
const std::vector<unsigned int> ordered_filament_ids = ui_ordered_filament_ids();
|
||||
if (ordered_filament_ids.size() <= 1)
|
||||
return;
|
||||
|
||||
wxDataViewItemArray sels;
|
||||
@@ -1059,24 +1104,16 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
|
||||
if (sels.Count() == 1) {
|
||||
const ModelConfig& config = obj_list()->get_item_config(sels[0]);
|
||||
// BBS: set default extruder to 1
|
||||
initial_extruder = config.has("extruder") ? config.extruder() : 1;
|
||||
initial_extruder = config.has("extruder") ? config.extruder() : 0;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= filaments_cnt; i++)
|
||||
for (size_t display_idx = 0; display_idx <= ordered_filament_ids.size(); ++display_idx)
|
||||
{
|
||||
bool is_active_extruder = i == initial_extruder;
|
||||
int icon_idx = i == 0 ? 0 : i - 1;
|
||||
const int actual_filament_id = display_idx == 0 ? 0 : int(ordered_filament_ids[display_idx - 1]);
|
||||
const bool is_active_extruder = actual_filament_id == initial_extruder;
|
||||
const int icon_idx = actual_filament_id == 0 ? 0 : actual_filament_id - 1;
|
||||
|
||||
wxString item_name = _L("Default");
|
||||
|
||||
if (i > 0) {
|
||||
auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]);
|
||||
if (preset == nullptr) {
|
||||
item_name = wxString::Format(_L("Filament %d"), i);
|
||||
} else {
|
||||
item_name = from_u8(preset->label(false));
|
||||
}
|
||||
}
|
||||
wxString item_name = filament_menu_item_name(actual_filament_id);
|
||||
|
||||
if (is_active_extruder) {
|
||||
item_name << " (" + _L("current") + ")";
|
||||
@@ -1084,11 +1121,13 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
|
||||
|
||||
if (icon_idx >= 0 && icon_idx < icons.size()) {
|
||||
append_menu_item(
|
||||
extruder_selection_menu, wxID_ANY, item_name, "", [i](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(i); }, *icons[icon_idx], menu,
|
||||
extruder_selection_menu, wxID_ANY, item_name, "",
|
||||
[actual_filament_id](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(actual_filament_id); }, *icons[icon_idx], menu,
|
||||
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
|
||||
} else {
|
||||
append_menu_item(
|
||||
extruder_selection_menu, wxID_ANY, item_name, "", [i](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(i); }, "", menu,
|
||||
extruder_selection_menu, wxID_ANY, item_name, "",
|
||||
[actual_filament_id](wxCommandEvent &) { obj_list()->set_extruder_for_selected_items(actual_filament_id); }, "", menu,
|
||||
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
|
||||
}
|
||||
}
|
||||
@@ -2213,8 +2252,8 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
|
||||
menu->Destroy(item_id);
|
||||
}
|
||||
|
||||
int filaments_cnt = filaments_count();
|
||||
if (filaments_cnt <= 1)
|
||||
const std::vector<unsigned int> ordered_filament_ids = ui_ordered_filament_ids();
|
||||
if (ordered_filament_ids.size() <= 1)
|
||||
return;
|
||||
|
||||
wxDataViewItemArray sels;
|
||||
@@ -2229,12 +2268,10 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
|
||||
}
|
||||
|
||||
std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
|
||||
if (icons.size() < filaments_cnt) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: icons size %1%, filaments_cnt=%2%")%icons.size()%filaments_cnt;
|
||||
if (icons.size() < ordered_filament_ids.size()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: icons size %1%, filaments_cnt=%2%") % icons.size() % ordered_filament_ids.size();
|
||||
if (icons.size() <= 1)
|
||||
return;
|
||||
else
|
||||
filaments_cnt = icons.size();
|
||||
}
|
||||
wxMenu* extruder_selection_menu = new wxMenu();
|
||||
const wxString& name = sels.Count() == 1 ? names[0] : names[1];
|
||||
@@ -2242,46 +2279,27 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
|
||||
int initial_extruder = -1; // negative value for multiple object/part selection
|
||||
if (sels.Count() == 1) {
|
||||
const ModelConfig& config = obj_list()->get_item_config(sels[0]);
|
||||
// BBS
|
||||
const auto sel_vol = obj_list()->get_selected_model_volume();
|
||||
if (sel_vol && sel_vol->type() == ModelVolumeType::PARAMETER_MODIFIER)
|
||||
initial_extruder = config.has("extruder") ? config.extruder() : 0;
|
||||
else
|
||||
initial_extruder = config.has("extruder") ? config.extruder() : 1;
|
||||
initial_extruder = config.has("extruder") ? config.extruder() : 0;
|
||||
}
|
||||
|
||||
// BBS
|
||||
bool has_modifier = false;
|
||||
for (auto sel : sels) {
|
||||
if (obj_list()->GetModel()->GetVolumeType(sel) == ModelVolumeType::PARAMETER_MODIFIER) {
|
||||
has_modifier = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = has_modifier ? 0 : 1; i <= filaments_cnt; i++)
|
||||
for (size_t display_idx = 0; display_idx <= ordered_filament_ids.size(); ++display_idx)
|
||||
{
|
||||
// BBS
|
||||
//bool is_active_extruder = i == initial_extruder;
|
||||
const int actual_filament_id = display_idx == 0 ? 0 : int(ordered_filament_ids[display_idx - 1]);
|
||||
bool is_active_extruder = false;
|
||||
|
||||
wxString item_name = _L("Default");
|
||||
|
||||
if (i > 0) {
|
||||
auto preset = wxGetApp().preset_bundle->filaments.find_preset(wxGetApp().preset_bundle->filament_presets[i - 1]);
|
||||
if (preset == nullptr) {
|
||||
item_name = wxString::Format(_L("Filament %d"), i);
|
||||
} else {
|
||||
item_name = from_u8(preset->label(false));
|
||||
}
|
||||
}
|
||||
wxString item_name = filament_menu_item_name(actual_filament_id);
|
||||
|
||||
if (is_active_extruder) {
|
||||
item_name << " (" + _L("current") + ")";
|
||||
}
|
||||
|
||||
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,
|
||||
[actual_filament_id](wxCommandEvent&) { obj_list()->set_extruder_for_selected_items(actual_filament_id); },
|
||||
actual_filament_id == 0 || size_t(actual_filament_id - 1) >= icons.size() ? wxNullBitmap : *icons[size_t(actual_filament_id - 1)], menu,
|
||||
[is_active_extruder]() { return !is_active_extruder; }, m_parent);
|
||||
}
|
||||
menu->Append(wxID_ANY, name, extruder_selection_menu, _L("Change Filament"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6536,9 +6551,9 @@ void ObjectList::OnEditingDone(wxDataViewEvent &event)
|
||||
// BBS: remove "const" qualifier
|
||||
void ObjectList::set_extruder_for_selected_items(const int extruder)
|
||||
{
|
||||
// BBS: check extruder id
|
||||
std::vector<std::string> colors = wxGetApp().plater()->get_extruder_colors_from_plater_config();
|
||||
if (extruder > colors.size())
|
||||
// Accept any configured filament id, including mixed virtual filaments.
|
||||
const int max_filaments = filaments_count();
|
||||
if (extruder < 0 || extruder > max_filaments)
|
||||
return;
|
||||
|
||||
wxDataViewItemArray sels;
|
||||
|
||||
@@ -2804,55 +2804,69 @@ int ObjectTablePanel::init_bitmap()
|
||||
m_undo_bitmap = create_scaled_bitmap("lock_normal", nullptr, 18);
|
||||
m_color_bitmaps = get_extruder_color_icons();
|
||||
|
||||
const std::vector<std::string> all_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, true);
|
||||
if (all_colors.size() > m_color_bitmaps.size()) {
|
||||
const double em = wxGetApp().em_unit();
|
||||
const int icon_width = int(4.4 * em + 0.5);
|
||||
const int icon_height = int(2.0 * em + 0.5);
|
||||
for (size_t idx = m_color_bitmaps.size(); idx < all_colors.size(); ++idx) {
|
||||
if (all_colors[idx].empty()) {
|
||||
m_color_bitmaps.push_back(nullptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
m_color_bitmaps.push_back(get_extruder_color_icon(all_colors[idx], std::to_string(idx + 1), icon_width, icon_height));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
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(nullptr, true);
|
||||
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;
|
||||
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))
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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: %s",
|
||||
i + 1,
|
||||
from_u8(mixed_filament_standardized_name(mf, physical_count)));
|
||||
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();
|
||||
@@ -433,7 +434,8 @@ void GLGizmoMmuSegmentation::on_render_input_window(float x, float y, float bott
|
||||
m_selected_extruder_idx = extruder_idx;
|
||||
}
|
||||
|
||||
if (extruder_idx < 16 && ImGui::IsItemHovered()) m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
|
||||
if (extruder_idx < int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT) && ImGui::IsItemHovered())
|
||||
m_imgui->tooltip(_L("Shortcut Key ") + std::to_string(extruder_idx + 1), max_tooltip_width);
|
||||
}
|
||||
// ORCA: Remap filaments section (Border only, Title in border).
|
||||
// Styled as a panel for visual grouping.
|
||||
@@ -731,6 +733,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 +757,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 +772,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();
|
||||
|
||||
@@ -72,11 +72,8 @@ public:
|
||||
|
||||
void data_changed(bool is_serializing) override;
|
||||
|
||||
// TriangleSelector::serialization/deserialization has a limit to store 19 different states.
|
||||
// EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored.
|
||||
// When increasing EXTRUDER_LIMIT, it needs to ensure that TriangleSelector::serialization/deserialization
|
||||
// will be also extended to support additional states, requiring at least one state to remain free out of 19 states.
|
||||
static const constexpr size_t EXTRUDERS_LIMIT = 16;
|
||||
// Keep the paint gizmo limit aligned with TriangleSelector state capacity.
|
||||
static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
|
||||
|
||||
const float get_cursor_radius_min() const override { return CursorRadiusMin; }
|
||||
|
||||
|
||||
@@ -998,16 +998,42 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
|
||||
keyCode = keyCode- WXK_NUMPAD0+'0';
|
||||
}
|
||||
if (keyCode >= '0' && keyCode <= '9') {
|
||||
if (keyCode == '1' && !m_timer_set_color.IsRunning()) {
|
||||
const int digit = keyCode - '0';
|
||||
const int shortcut_max = int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT);
|
||||
auto select_shortcut = [mmu_seg](int number) {
|
||||
return number > 0 && mmu_seg->on_number_key_down(number);
|
||||
};
|
||||
|
||||
if (m_timer_set_color.IsRunning() && m_pending_color_shortcut_tens > 0) {
|
||||
const int two_digit_shortcut = m_pending_color_shortcut_tens * 10 + digit;
|
||||
if (two_digit_shortcut <= shortcut_max) {
|
||||
processed = select_shortcut(two_digit_shortcut);
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
m_timer_set_color.Stop();
|
||||
} else {
|
||||
// Fall back to the pending single-digit shortcut and then process current digit as fresh input.
|
||||
processed = select_shortcut(m_pending_color_shortcut_tens);
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
m_timer_set_color.Stop();
|
||||
|
||||
const bool can_start_two_digit = digit > 0 && digit * 10 <= shortcut_max;
|
||||
if (can_start_two_digit) {
|
||||
m_pending_color_shortcut_tens = digit;
|
||||
m_timer_set_color.StartOnce(500);
|
||||
processed = true;
|
||||
} else {
|
||||
processed = select_shortcut(digit) || processed;
|
||||
}
|
||||
else if (keyCode < '7' && m_timer_set_color.IsRunning()) {
|
||||
processed = mmu_seg->on_number_key_down(keyCode - '0'+10);
|
||||
m_timer_set_color.Stop();
|
||||
}
|
||||
else {
|
||||
processed = mmu_seg->on_number_key_down(keyCode - '0');
|
||||
} else {
|
||||
const bool can_start_two_digit = digit > 0 && digit * 10 <= shortcut_max;
|
||||
if (can_start_two_digit) {
|
||||
m_pending_color_shortcut_tens = digit;
|
||||
m_timer_set_color.StartOnce(500);
|
||||
processed = true;
|
||||
} else {
|
||||
processed = select_shortcut(digit);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (keyCode == 'F' || keyCode == 'T' || keyCode == 'S' || keyCode == 'C' || keyCode == 'H' || keyCode == 'G') {
|
||||
@@ -1054,12 +1080,15 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
|
||||
|
||||
void GLGizmosManager::on_set_color_timer(wxTimerEvent& evt)
|
||||
{
|
||||
if (m_current == MmSegmentation) {
|
||||
if (m_current == MmSegmentation && m_pending_color_shortcut_tens > 0) {
|
||||
GLGizmoMmuSegmentation* mmu_seg = dynamic_cast<GLGizmoMmuSegmentation*>(get_current());
|
||||
mmu_seg->on_number_key_down(1);
|
||||
if (mmu_seg != nullptr) {
|
||||
mmu_seg->on_number_key_down(m_pending_color_shortcut_tens);
|
||||
m_parent.set_as_dirty();
|
||||
}
|
||||
}
|
||||
m_pending_color_shortcut_tens = 0;
|
||||
}
|
||||
|
||||
void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot)
|
||||
{
|
||||
|
||||
@@ -144,6 +144,7 @@ private:
|
||||
|
||||
//When there are more than 9 colors, shortcut key coloring
|
||||
wxTimer m_timer_set_color;
|
||||
int m_pending_color_shortcut_tens = 0;
|
||||
void on_set_color_timer(wxTimerEvent& evt);
|
||||
|
||||
// key MENU_ICON_NAME, value = ImtextureID
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -166,6 +166,8 @@ enum class NotificationType
|
||||
OrcaSharedProfilesAvailable,
|
||||
OrcaCloudAPIError,
|
||||
OrcaSyncConflict,
|
||||
BBLMixedFilamentBroken,
|
||||
BBLSingleExtruderMixedFilamentRisk,
|
||||
NotificationTypeCount
|
||||
|
||||
};
|
||||
|
||||
@@ -423,15 +423,31 @@ void ObjectDataViewModelNode::UpdateExtruderAndColorIcon(wxString extruder /*= "
|
||||
}
|
||||
}
|
||||
|
||||
const size_t extruder_id_1based = extruder_idx;
|
||||
if (extruder_idx > 0) --extruder_idx;
|
||||
|
||||
// Create the bitmap with color bars.
|
||||
std::vector<wxBitmap*> bmps = get_extruder_color_icons(false);// use wide icons
|
||||
if (bmps.empty()) {
|
||||
m_extruder_bmp = wxNullBitmap;
|
||||
if (!bmps.empty() && extruder_idx < bmps.size()) {
|
||||
m_extruder_bmp = *bmps[extruder_idx];
|
||||
return;
|
||||
}
|
||||
|
||||
m_extruder_bmp = *bmps[extruder_idx >= bmps.size() ? 0 : extruder_idx];
|
||||
// Fallback for mixed virtual filaments when the shared icon vector only covers physical filaments.
|
||||
if (wxGetApp().plater() != nullptr) {
|
||||
const std::vector<std::string> all_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, true);
|
||||
if (extruder_id_1based >= 1 && extruder_id_1based <= all_colors.size() && !all_colors[extruder_id_1based - 1].empty()) {
|
||||
const double em = wxGetApp().em_unit();
|
||||
const int icon_width = int(4.4 * em + 0.5);
|
||||
const int icon_height = int(2.0 * em + 0.5);
|
||||
if (wxBitmap *bmp = get_extruder_color_icon(all_colors[extruder_id_1based - 1], std::to_string(extruder_id_1based), icon_width, icon_height)) {
|
||||
m_extruder_bmp = *bmp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_extruder_bmp = bmps.empty() ? wxNullBitmap : *bmps.front();
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+1284
-29
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1646,6 +1646,49 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
}
|
||||
}
|
||||
|
||||
if (opt_key == "dithering_local_z_mode") {
|
||||
const bool local_z_enabled = boost::any_cast<bool>(value);
|
||||
if (local_z_enabled &&
|
||||
(!m_config->has("mixed_filament_region_collapse") ||
|
||||
m_config->option("mixed_filament_region_collapse") == nullptr ||
|
||||
m_config->opt_bool("mixed_filament_region_collapse"))) {
|
||||
change_opt_value(*m_config, "mixed_filament_region_collapse", boost::any(false));
|
||||
if (m_type == Preset::TYPE_PRINT) {
|
||||
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
|
||||
project_cfg.set_key_value("mixed_filament_region_collapse", new ConfigOptionBool(false));
|
||||
}
|
||||
if (Field *field = this->get_field("mixed_filament_region_collapse"))
|
||||
field->set_value(boost::any(false), false);
|
||||
update_dirty();
|
||||
}
|
||||
if (!local_z_enabled &&
|
||||
m_config->has("dithering_local_z_whole_objects") &&
|
||||
m_config->option("dithering_local_z_whole_objects") != nullptr &&
|
||||
m_config->opt_bool("dithering_local_z_whole_objects")) {
|
||||
change_opt_value(*m_config, "dithering_local_z_whole_objects", boost::any(false));
|
||||
if (m_type == Preset::TYPE_PRINT) {
|
||||
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
|
||||
project_cfg.set_key_value("dithering_local_z_whole_objects", new ConfigOptionBool(false));
|
||||
}
|
||||
if (Field *field = this->get_field("dithering_local_z_whole_objects"))
|
||||
field->set_value(boost::any(false), false);
|
||||
update_dirty();
|
||||
}
|
||||
if (!local_z_enabled &&
|
||||
m_config->has("dithering_local_z_direct_multicolor") &&
|
||||
m_config->option("dithering_local_z_direct_multicolor") != nullptr &&
|
||||
m_config->opt_bool("dithering_local_z_direct_multicolor")) {
|
||||
change_opt_value(*m_config, "dithering_local_z_direct_multicolor", boost::any(false));
|
||||
if (m_type == Preset::TYPE_PRINT) {
|
||||
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
|
||||
project_cfg.set_key_value("dithering_local_z_direct_multicolor", new ConfigOptionBool(false));
|
||||
}
|
||||
if (Field *field = this->get_field("dithering_local_z_direct_multicolor"))
|
||||
field->set_value(boost::any(false), false);
|
||||
update_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
// reload scene to update timelapse wipe tower
|
||||
if (opt_key == "timelapse_type") {
|
||||
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value;
|
||||
@@ -1961,10 +2004,42 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
return;
|
||||
}
|
||||
|
||||
const bool refresh_mixed_filament_panel =
|
||||
m_type == Preset::TYPE_PRINT && opt_key == "mixed_filament_component_bias_enabled";
|
||||
|
||||
// Keep Mixed Filaments global settings in sync with project_config. In
|
||||
// full_fff_config(), project_config is applied last and would otherwise
|
||||
// override the edited print preset value from the Others panel.
|
||||
if (m_type == Preset::TYPE_PRINT &&
|
||||
(opt_key == "mixed_filament_gradient_mode" ||
|
||||
opt_key == "mixed_filament_height_lower_bound" ||
|
||||
opt_key == "mixed_filament_height_upper_bound" ||
|
||||
opt_key == "mixed_color_layer_height_a" ||
|
||||
opt_key == "mixed_color_layer_height_b" ||
|
||||
opt_key == "mixed_filament_advanced_dithering" ||
|
||||
opt_key == "mixed_filament_pointillism_pixel_size" ||
|
||||
opt_key == "mixed_filament_pointillism_line_gap" ||
|
||||
opt_key == "mixed_filament_component_bias_enabled" ||
|
||||
opt_key == "mixed_filament_surface_indentation" ||
|
||||
opt_key == "mixed_filament_region_collapse" ||
|
||||
opt_key == "dithering_z_step_size" ||
|
||||
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_definitions")) {
|
||||
DynamicPrintConfig &project_cfg = wxGetApp().preset_bundle->project_config;
|
||||
if (const ConfigOption *opt = m_config->option(opt_key))
|
||||
project_cfg.set_key_value(opt_key, opt->clone());
|
||||
}
|
||||
|
||||
update();
|
||||
if(m_active_page)
|
||||
m_active_page->update_visibility(m_mode, true);
|
||||
m_page_view->GetParent()->Layout();
|
||||
|
||||
if (refresh_mixed_filament_panel && wxGetApp().plater() != nullptr)
|
||||
wxGetApp().sidebar().update_mixed_filament_panel(false);
|
||||
}
|
||||
|
||||
void Tab::show_timelapse_warning_dialog() {
|
||||
@@ -2318,6 +2393,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 +2545,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 +2746,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 +2810,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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -783,6 +783,24 @@ void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl,
|
||||
bool use_thin_icon/* = false*/)
|
||||
{
|
||||
std::vector<wxBitmap*> icons = get_extruder_color_icons(use_thin_icon);
|
||||
if (dynamic_cast<Slic3r::GUI::ObjectList*>(parent) != nullptr && Slic3r::GUI::wxGetApp().plater() != nullptr) {
|
||||
const std::vector<std::string> all_colors =
|
||||
Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, true);
|
||||
|
||||
if (all_colors.size() > icons.size()) {
|
||||
const double em = Slic3r::GUI::wxGetApp().em_unit();
|
||||
const int icon_width = int((use_thin_icon ? 2.0 : 4.4) * em + 0.5);
|
||||
const int icon_height = int(2.0 * em + 0.5);
|
||||
for (size_t idx = icons.size(); idx < all_colors.size(); ++idx) {
|
||||
if (all_colors[idx].empty()) {
|
||||
icons.push_back(nullptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
icons.push_back(get_extruder_color_icon(all_colors[idx], std::to_string(idx + 1), icon_width, icon_height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!*ctrl) {
|
||||
*ctrl = new Slic3r::GUI::BitmapComboBox(parent, wxID_ANY, wxEmptyString, pos, size, 0, nullptr, wxCB_READONLY);
|
||||
@@ -811,13 +829,13 @@ void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl,
|
||||
for (wxBitmap* bmp : icons) {
|
||||
if (i == 0) {
|
||||
if (!first_item.empty())
|
||||
(*ctrl)->Append(_(first_item), *bmp);
|
||||
(*ctrl)->Append(_(first_item), bmp ? *bmp : wxNullBitmap);
|
||||
++i;
|
||||
}
|
||||
|
||||
(*ctrl)->Append(use_full_item_name
|
||||
? Slic3r::GUI::from_u8((boost::format("%1% %2%") % str % i).str())
|
||||
: wxString::Format("%d", i), *bmp);
|
||||
: wxString::Format("%d", i), bmp ? *bmp : wxNullBitmap);
|
||||
++i;
|
||||
}
|
||||
(*ctrl)->SetSelection(0);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,189 @@
|
||||
#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 <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[review-fixes] clear_local_z_plan called from clear_layers", "[review-fixes][mixed-filament]")
|
||||
{
|
||||
// Sentinel: ensure the source contains the 3 invalidation hooks the
|
||||
// FullSpectrum verification report flagged as missing. This is a
|
||||
// smoke-test against the source so we can detect future regressions
|
||||
// without depending on a full slicing-pipeline harness.
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path repo = fs::path(__FILE__).parent_path().parent_path().parent_path();
|
||||
const fs::path src = repo / "src" / "libslic3r" / "PrintObject.cpp";
|
||||
REQUIRE(fs::exists(src));
|
||||
|
||||
std::ifstream in(src.string());
|
||||
std::stringstream buf; buf << in.rdbuf();
|
||||
const std::string body = buf.str();
|
||||
|
||||
const auto count_substr = [&](const std::string &needle) {
|
||||
size_t n = 0, pos = 0;
|
||||
while ((pos = body.find(needle, pos)) != std::string::npos) { ++n; ++pos; }
|
||||
return n;
|
||||
};
|
||||
|
||||
// Three FS-mandated call sites: clear_layers, invalidate_step(posSlice),
|
||||
// invalidate_all_steps. Plus the one already-present site inside
|
||||
// build_local_z_plan(). PrintObject.cpp itself has 3 (4 once the
|
||||
// backport is complete).
|
||||
REQUIRE(count_substr("this->clear_local_z_plan()") >= 3);
|
||||
}
|
||||
|
||||
TEST_CASE("[review-fixes] merge_segmented_layers preserves channel 0", "[review-fixes][mixed-filament]")
|
||||
{
|
||||
// Sentinel: ensure merge_segmented_layers uses the FS shape
|
||||
// (output sized num_facets_states, channel 0 = default), not the
|
||||
// pre-FS-a11b70e3a shape (output sized num_facets_states - 1,
|
||||
// channel 0 dropped). The FS-verbatim apply_mm_segmentation in
|
||||
// PrintObjectSlice.cpp expects channel 0 to be present; mismatching
|
||||
// shapes silently shifts every painted region's filament_id by -1
|
||||
// (e.g. paint with mixed slot 4 -> applied as physical filament 3).
|
||||
namespace fs = boost::filesystem;
|
||||
const fs::path repo = fs::path(__FILE__).parent_path().parent_path().parent_path();
|
||||
const fs::path src = repo / "src" / "libslic3r" / "MultiMaterialSegmentation.cpp";
|
||||
REQUIRE(fs::exists(src));
|
||||
|
||||
std::ifstream in(src.string());
|
||||
std::stringstream buf; buf << in.rdbuf();
|
||||
const std::string body = buf.str();
|
||||
|
||||
// Producer must NOT subtract one from num_facets_states when sizing the output.
|
||||
REQUIRE(body.find("num_facets_states - 1") == std::string::npos);
|
||||
// Producer must NOT shift indexing back by one when writing into the output.
|
||||
REQUIRE(body.find("[extruder_id - 1]") == std::string::npos);
|
||||
// Loop must include channel 0 (extruder_id starts at 0, not 1).
|
||||
REQUIRE(body.find("for (size_t extruder_id = 0; extruder_id < num_facets_states") != std::string::npos);
|
||||
}
|
||||
Reference in New Issue
Block a user