Compare commits

...
Author SHA1 Message Date
Ian Bassi daa75b3fa0 Revert "Forced pattern , separed to > 9 extruder"
This reverts commit 42b506ad85.
2026-06-10 08:27:02 -03:00
Ian Bassi 42b506ad85 Forced pattern , separed to > 9 extruder 2026-06-01 11:40:31 -03:00
Ian Bassi 84c7186674 Support extended triangle paint states up to 32
Extend TriangleSelector serialization to encode leaf facet paint states up to 32 by adding an escaped second nibble (12-bit path: wwwwzzzzxxyy). Implemented a push_nibble helper and an extended-prefix (0b1110) to signal the extra nibble. Updated deserialize, update_used_states and has_facets to decode the new format while preserving legacy behavior for 3..16. Adjusted asserts and state handling accordingly. Added a unit test and include for TriangleSelector to verify round-trip of paint states above 16.
2026-06-01 09:24:55 -03:00
Ian Bassi 9670da7ddd Increase selecting color shortcut 2026-06-01 09:24:54 -03:00
Ian Bassi c858360ee6 Paint up to 32 2026-06-01 09:24:54 -03:00
Ian Bassi 1ecedd3187 Improve virtual color namings and added for parts 2026-06-01 09:24:54 -03:00
Ian Bassi 0346c4931a Allow mixed filament for Filament for features
Preserve mixed-physical filament handling and UI separation. Clamp filament indices in PresetBundle using total mixed filament count and treat support vs feature slots differently. Update ConfigManipulation to forbid mixed (virtual) IDs for support slots while allowing mixed IDs for feature slots, and reset out-of-range values. Refactor Plater DynamicFilamentList to support an include_mixed mode, improve labels/icons and ID mapping, and introduce separate dynamic_feature_filament_list and dynamic_support_filament_list instances (registered per choice). Add update_dynamic_filament_list() and use it when filaments change or sync occurs.
2026-06-01 09:24:54 -03:00
Ian Bassi 227bdb77da Preserve explicit feature filament overrides
Keep legacy "Filament for Features" behavior by treating an explicit sparse-infill filament choice as an override even when enable_infill_filament_override is not set. In ToolOrdering.cpp add explicit_sparse_override detection (sparse_infill_filament != wall_filament and both > 0) so base-infill logic respects explicit feature filament selection. In PrintObject.cpp stop clearing feature override flags when an extruder is set to "Default" (0), so parent-scope feature overrides are preserved instead of being reset. This avoids unintentionally removing feature filament overrides when a scope uses the default extruder.
2026-06-01 09:24:54 -03:00
SoftFever 2a8782b8f6 fix build errors 2026-06-01 09:24:54 -03:00
SoftFever c0dfe50bc5 Update from FS at b3c41fda4.
Slicing
  - align merge_segmented_layers shape with FS apply_mm_segmentation
    (size = num_facets_states, loop from 0, no -1 shift); painted mixed
    regions were previously attributed to filament_id-1 of intent.
    apply_fuzzy_skin_segmentation reads channel 1;
    apply_mixed_surface_indentation uses segmentation_channel_filament_id
  - port apply_mixed_surface_indentation, apply_mixed_component_surface_offsets,
    apply_mixed_region_surface_offsets, apply_surface_emboss_mixed_region_override,
    plus surface_emboss_mixed_* debug subsystem
  - refactor apply_mm_segmentation (by-value MM, bias_mode, surface-type-
    preserving intersection, region normalization, post-MM dump); hoist MM
    segmentation into slice_volumes so mixed apply_* flow can mutate it
  - restore clear_local_z_plan() invalidation hooks
    (PrintObject.cpp:805/1264/1286)

  GCode
  - add LayerTools::preserve_extruder_order, honored by collect_extruders,
    both reorder_extruders overloads, and
    reorder_filaments_for_minimum_flush_volume; helpers
    append_unique_preserve_order / remove_duplicates_preserve_order
  - wire MixedFilamentManager::ordered_perimeter_extruders for grouped
    manual-pattern walls; set preserve_extruder_order when >= 2
  - mixed-aware support: layer_height set for support-only layers,
    ExtrusionRole-based has_support/has_interface with erMixed short-
    circuit, support_filament / support_interface_filament routed through
    resolve_mixed

  Print
  - materialize mixed_filament_pointillism_{pixel_size,line_gap} in
    PrintApply's option-tracking block so in-session edits diff correctly

  GUI
  - Tab::on_value_change: dithering_local_z_mode cascading clears, 17-key
    project_config sync, update_mixed_filament_panel(false) on
    mixed_filament_component_bias_enabled change
  - GUI_Factories: physical_filaments_count, ui_ordered_filament_ids,
    filament_menu_item_name; filaments_count includes enabled mixed
    virtuals; right-click 'Change filament' submenus iterate UI-ordered IDs

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

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

Update TriangleSelector.cpp

Co-Authored-By: Rad <radugheorghiu96@gmail.com>
Co-Authored-By: Justin Hayes <justinh@rahb.ca>
Co-Authored-By: Calogero Guagenti <calogeroguagenti@gmail.com>
Co-Authored-By: xSil3nt <ahmedshazin21@gmail.com>
Co-Authored-By: ratdoux <62392831+ratdoux@users.noreply.github.com>
2026-06-01 09:24:53 -03:00
74 changed files with 20941 additions and 483 deletions
+171
View File
@@ -0,0 +1,171 @@
# Mixed Filament
Ported from [OrcaSlicer-FullSpectrum](https://github.com/SoftFever/OrcaSlicer-FullSpectrum)
with contributions from Rad, Justin Hayes, Calogero Guagenti, xSil3nt, and ratdoux.
---
## User Guide
### What It Does
Mixed Filament lets a single virtual filament slot alternate between two
physical filaments across layers (or within a layer in Pointillisme mode),
producing blended or gradient-like colours on single-extruder printers.
### Enabling
1. Load a multi-colour (multi-extruder) profile with at least 2 filaments.
2. The **Mixed Filaments** panel appears automatically in the right-hand sidebar
when 2 or more filaments are configured.
3. Each auto-generated row represents one pair of physical filaments.
Toggle a row to enable it; the total filament count grows to include the
virtual slot.
### Sidebar Workflow
- **Add** — creates a custom row for the same pair or a different ratio.
- **Edit** — opens `MixedFilamentConfigPanel` to adjust ratio, pattern,
surface offset (bias), and distribution mode.
- **Delete** — marks the row deleted; existing painted geometry retains its
virtual filament ID until you re-slice or repaint.
Painting with a virtual filament ID behaves the same as painting with a
physical one — use the Multi-Material Painting gizmo and select the virtual
slot from the colour palette.
### Color Match Dialog
Open via the colour swatch on a mixed row. The dialog (`MixedFilamentColorMatchDialog`)
shows a live preview strip of the blended result and lets you adjust ratio
until the preview matches your target colour. The preview accounts for
surface-offset bias when enabled.
### Anti-Banding Options
| Setting | What it does |
|---|---|
| `mixed_filament_advanced_dithering` | Uses an ordered dither pattern instead of simple A-then-B runs. Reduces stripe visibility on some hue pairs. More experimental than the default. |
| `dithering_local_z_mode` | Splits each blended layer into two sub-layers whose heights are proportional to the mix ratio (e.g. 66/33 at 0.12 mm → 0.08 mm + 0.04 mm). Produces the smoothest colour gradients. |
| `dithering_local_z_whole_objects` | Extends Local-Z splitting beyond painted masks to cover the entire object cross-section. Useful when mixed walls surround a painted zone. |
| `dithering_local_z_direct_multicolor` | For rows with 3 or more physical components, allocates Local-Z sub-layers directly across all components with carry-over error correction instead of collapsing to pair cadence. More toolchanges; less banding. |
### Gotchas
- **Single-extruder warning** — Mixed Filament requires a physical toolchange
between the two components. On a true single-nozzle printer this means a
manual filament swap. Verify your printer profile supports `T0`/`T1` before
using mixed slots in a production print.
- **Variable-layer interaction** — If Variable Layer Height is enabled, Local-Z
sub-layer heights are recomputed per interval. The mix ratio is preserved but
the absolute sub-layer heights change with the variable height. Review the
layer preview after applying variable layers.
- **Custom sequence disabled** — OrcaSlicer's "custom toolchange sequence" is
suppressed when mixed filaments are active (`PlateSettingsDialog`). The
virtual-to-physical resolution must control toolchange order; a user-defined
sequence would break it.
- **Stable IDs** — each mixed row carries a `stable_id` (64-bit). If you
reorder or delete rows and then load an older project, the ID remap in
`PresetBundle::update_mixed_filament_id_remap` translates painted geometry
to the correct new virtual slot. Do not rely on the 1-based filament index
as a stable identifier.
---
## Developer Guide
### Core Data Structures
```
src/libslic3r/MixedFilament.hpp — MixedFilament struct, MixedFilamentManager
src/libslic3r/MixedFilament.cpp — serialization, resolve(), auto_generate()
src/libslic3r/LocalZOrderOptimizer.hpp — bucket-ordering helpers for Local-Z
```
The key scalar fields on `MixedFilament`:
- `component_a`, `component_b` — 1-based physical filament indices.
- `ratio_a`, `ratio_b` — layer-alternation cadence numerators.
- `mix_b_percent` — nominal colour mix (used for Local-Z height computation
and the Color Match preview; does not change the cadence).
- `stable_id` — monotonically increasing 64-bit ID assigned at construction.
Never reused. Survives serialization round-trips.
- `distribution_mode` — selects between `Simple`, `SameLayerPointillisme`,
and `GroupedManual`.
### Seam: Adding New Distribution Modes
`MixedFilamentManager::resolve()` in `MixedFilament.cpp` is the single
dispatch point that maps `(virtual_filament_id, num_physical, layer_index)`
to a physical extruder. The current switch covers `Simple` and
`SameLayerPointillisme`. A new mode is added by:
1. Adding a value to the `MixedFilament::DistributionMode` enum in
`MixedFilament.hpp`.
2. Adding a `case` to `MixedFilamentManager::resolve()` in `MixedFilament.cpp`.
3. Serializing the new mode token in `serialize_custom_entries` /
`load_custom_entries` (format is a semicolon-delimited row string; see
existing tokens for the convention).
G-code emission (`src/libslic3r/GCode/`) reads only the physical ID returned
by `resolve()`, so new modes are automatically emitted without further changes.
### Seam: New Toolchange-Cost Heuristics
`LocalZOrderOptimizer` (`src/libslic3r/LocalZOrderOptimizer.hpp`) exposes:
- `order_bucket_extruders(bucket, current, preferred_last)` — reorders a
single-layer bucket to minimise toolchanges given the current active extruder.
- `order_pass_group(group, current_extruder)` — greedy walk across a set of
buckets (one per Local-Z sub-layer) to minimise total transitions.
To add a new heuristic (e.g. cost-based look-ahead), replace or wrap
`order_pass_group`. The caller in `PrintObjectSlice.cpp` passes the result
directly into the sub-layer plan, so the heuristic is fully decoupled from
the plan builder.
### Seam: New Picker Shapes in the Color Map Panel
`MixedFilamentColorMapPanel` (`src/slic3r/GUI/MixedFilamentColorMapPanel.hpp`)
renders a 2-D colour map using a set of geometry "types" (currently strip and
gradient). Each type is a small self-contained rendering path keyed by an enum
value. New shapes are added by:
1. Adding an enum value to `MixedFilamentColorMapPanel::GeometryType`.
2. Implementing the corresponding `Paint*` helper (follow `PaintStrip` as a
template).
3. Wiring the new type into the `switch` in `OnPaint`.
### Persistence (3MF)
The entire mixed-filament state is stored as a single string key
`mixed_filament_definitions` in the project config block (section `[presets]`
in the 3MF metadata).
Round-trip path:
```
MixedFilamentManager::serialize_custom_entries()
called by PresetBundle::sync_mixed_filaments_to_config()
written by bbs_3mf: store_bbs_3mf → config.set("presets", "mixed_filament_definitions", ...)
load_bbs_3mf → config.get("presets", "mixed_filament_definitions")
stored in project_config["mixed_filament_definitions"]
read by PresetBundle::sync_mixed_filaments_from_config()
→ mixed_filaments.auto_generate(colours)
→ mixed_filaments.load_custom_entries(defs, colours)
```
Auto-generated rows are *not* written to the definitions string; they are
rebuilt from the filament colour list. Only `custom == true` rows are stored.
See `tests/fff_print/test_mixed_filament_e2e.cpp` for regression tests
covering this path.
### ID Remap
When filaments are added, removed, or reordered, virtual IDs shift.
`PresetBundle::update_mixed_filament_id_remap(old_mixed, old_count, new_count)`
produces a `remap` vector where `remap[old_virtual_id] = new_virtual_id`.
Painted triangle mesh face data uses these IDs; the remap is applied in
`TriangleSelectorMixed` after any filament list change.
+4
View File
@@ -250,3 +250,7 @@ src/slic3r/GUI/RammingChart.cpp
src/slic3r/GUI/StepMeshDialog.cpp src/slic3r/GUI/StepMeshDialog.cpp
src/slic3r/GUI/FilamentPickerDialog.hpp src/slic3r/GUI/FilamentPickerDialog.hpp
src/libslic3r/PresetBundle.cpp src/libslic3r/PresetBundle.cpp
src/slic3r/GUI/MixedFilamentColorMatchDialog.cpp
src/slic3r/GUI/MixedFilamentColorMatchDialog.hpp
src/slic3r/GUI/MixedFilamentConfigPanel.cpp
src/slic3r/GUI/MixedFilamentConfigPanel.hpp
+4
View File
@@ -408,6 +408,10 @@ void AppConfig::set_defaults()
set("auto_calculate_flush","all"); set("auto_calculate_flush","all");
} }
if (get("auto_generate_gradients").empty()) {
set_bool("auto_generate_gradients", true);
}
if (get("show_canvas_zoom_button").empty()) { if (get("show_canvas_zoom_button").empty()) {
set_bool("show_canvas_zoom_button", true); set_bool("show_canvas_zoom_button", true);
} }
+6
View File
@@ -176,6 +176,9 @@ set(lisbslic3r_sources
Fill/Lightning/Layer.hpp Fill/Lightning/Layer.hpp
Fill/Lightning/TreeNode.cpp Fill/Lightning/TreeNode.cpp
Fill/Lightning/TreeNode.hpp Fill/Lightning/TreeNode.hpp
filament_mixer.cpp
filament_mixer.h
filament_mixer_model.h
Flow.cpp Flow.cpp
Flow.hpp Flow.hpp
FlushVolCalc.cpp FlushVolCalc.cpp
@@ -286,6 +289,7 @@ set(lisbslic3r_sources
Line.hpp Line.hpp
LocalesUtils.cpp LocalesUtils.cpp
LocalesUtils.hpp LocalesUtils.hpp
LocalZOrderOptimizer.hpp
MarchingSquares.hpp MarchingSquares.hpp
Measure.cpp Measure.cpp
Measure.hpp Measure.hpp
@@ -295,6 +299,8 @@ set(lisbslic3r_sources
MinAreaBoundingBox.hpp MinAreaBoundingBox.hpp
MinimumSpanningTree.cpp MinimumSpanningTree.cpp
MinimumSpanningTree.hpp MinimumSpanningTree.hpp
MixedFilament.cpp
MixedFilament.hpp
miniz_extension.cpp miniz_extension.cpp
miniz_extension.hpp miniz_extension.hpp
ModelArrange.cpp ModelArrange.cpp
+56 -16
View File
@@ -170,7 +170,8 @@ public:
ExtrusionPath(ExtrusionRole role, double mm3_per_mm, float width, float height, bool no_extrusion = false) : mm3_per_mm(mm3_per_mm), width(width), height(height), m_role(role), m_no_extrusion(no_extrusion) {} ExtrusionPath(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) ExtrusionPath(const ExtrusionPath &rhs)
: polyline(rhs.polyline) : ExtrusionEntity(rhs)
, polyline(rhs.polyline)
, overhang_degree(rhs.overhang_degree) , overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree) , curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm) , mm3_per_mm(rhs.mm3_per_mm)
@@ -181,9 +182,12 @@ public:
, m_can_reverse(rhs.m_can_reverse) , m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role) , m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion) , m_no_extrusion(rhs.m_no_extrusion)
{} {
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath(ExtrusionPath &&rhs) ExtrusionPath(ExtrusionPath &&rhs)
: polyline(std::move(rhs.polyline)) : ExtrusionEntity(rhs)
, polyline(std::move(rhs.polyline))
, overhang_degree(rhs.overhang_degree) , overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree) , curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm) , mm3_per_mm(rhs.mm3_per_mm)
@@ -194,9 +198,12 @@ public:
, m_can_reverse(rhs.m_can_reverse) , m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role) , m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion) , m_no_extrusion(rhs.m_no_extrusion)
{} {
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath(const Polyline3 &polyline, const ExtrusionPath &rhs) ExtrusionPath(const Polyline3 &polyline, const ExtrusionPath &rhs)
: polyline(polyline) : ExtrusionEntity(rhs)
, polyline(polyline)
, overhang_degree(rhs.overhang_degree) , overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree) , curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm) , mm3_per_mm(rhs.mm3_per_mm)
@@ -207,9 +214,12 @@ public:
, m_can_reverse(rhs.m_can_reverse) , m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role) , m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion) , m_no_extrusion(rhs.m_no_extrusion)
{} {
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath(Polyline3 &&polyline, const ExtrusionPath &rhs) ExtrusionPath(Polyline3 &&polyline, const ExtrusionPath &rhs)
: polyline(std::move(polyline)) : ExtrusionEntity(rhs)
, polyline(std::move(polyline))
, overhang_degree(rhs.overhang_degree) , overhang_degree(rhs.overhang_degree)
, curve_degree(rhs.curve_degree) , curve_degree(rhs.curve_degree)
, mm3_per_mm(rhs.mm3_per_mm) , mm3_per_mm(rhs.mm3_per_mm)
@@ -220,7 +230,9 @@ public:
, m_can_reverse(rhs.m_can_reverse) , m_can_reverse(rhs.m_can_reverse)
, m_role(rhs.m_role) , m_role(rhs.m_role)
, m_no_extrusion(rhs.m_no_extrusion) , m_no_extrusion(rhs.m_no_extrusion)
{} {
this->inset_idx = rhs.inset_idx;
}
ExtrusionPath& operator=(const ExtrusionPath& rhs) { ExtrusionPath& operator=(const ExtrusionPath& rhs) {
m_can_reverse = rhs.m_can_reverse; m_can_reverse = rhs.m_can_reverse;
@@ -234,6 +246,7 @@ public:
this->overhang_degree = rhs.overhang_degree; this->overhang_degree = rhs.overhang_degree;
this->curve_degree = rhs.curve_degree; this->curve_degree = rhs.curve_degree;
this->polyline = rhs.polyline; this->polyline = rhs.polyline;
this->inset_idx = rhs.inset_idx;
return *this; return *this;
} }
ExtrusionPath& operator=(ExtrusionPath&& rhs) { ExtrusionPath& operator=(ExtrusionPath&& rhs) {
@@ -248,6 +261,7 @@ public:
this->overhang_degree = rhs.overhang_degree; this->overhang_degree = rhs.overhang_degree;
this->curve_degree = rhs.curve_degree; this->curve_degree = rhs.curve_degree;
this->polyline = std::move(rhs.polyline); this->polyline = std::move(rhs.polyline);
this->inset_idx = rhs.inset_idx;
return *this; return *this;
} }
@@ -380,20 +394,31 @@ public:
ExtrusionPaths paths; ExtrusionPaths paths;
ExtrusionMultiPath() {} ExtrusionMultiPath() {}
ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : paths(rhs.paths), m_can_reverse(rhs.m_can_reverse) {} ExtrusionMultiPath(const ExtrusionMultiPath &rhs) : ExtrusionEntity(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(ExtrusionMultiPath &&rhs) : ExtrusionEntity(rhs), paths(std::move(rhs.paths)), m_can_reverse(rhs.m_can_reverse) {}
ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths) {} ExtrusionMultiPath(const ExtrusionPaths &paths) : paths(paths)
ExtrusionMultiPath(const ExtrusionPath &path) {this->paths.push_back(path); m_can_reverse = path.can_reverse(); } {
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) ExtrusionMultiPath &operator=(const ExtrusionMultiPath &rhs)
{ {
this->paths = rhs.paths; this->paths = rhs.paths;
this->inset_idx = rhs.inset_idx;
m_can_reverse = rhs.m_can_reverse; m_can_reverse = rhs.m_can_reverse;
return *this; return *this;
} }
ExtrusionMultiPath &operator=(ExtrusionMultiPath &&rhs) ExtrusionMultiPath &operator=(ExtrusionMultiPath &&rhs)
{ {
this->paths = std::move(rhs.paths); this->paths = std::move(rhs.paths);
this->inset_idx = rhs.inset_idx;
m_can_reverse = rhs.m_can_reverse; m_can_reverse = rhs.m_can_reverse;
return *this; return *this;
} }
@@ -447,12 +472,27 @@ public:
ExtrusionPaths paths; ExtrusionPaths paths;
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {} ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), 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) {} {
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) 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) 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 is_loop() const override{ return true; }
bool can_reverse() const override { return false; } bool can_reverse() const override { return false; }
ExtrusionEntity* clone() const override{ return new ExtrusionLoop (*this); } ExtrusionEntity* clone() const override{ return new ExtrusionLoop (*this); }
+27 -1
View File
@@ -11,6 +11,7 @@
#include "../Time.hpp" #include "../Time.hpp"
#include "../I18N.hpp" #include "../I18N.hpp"
#include "../MixedFilament.hpp"
#include "bbs_3mf.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"); 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) { for (ModelObject* mo : m_model->objects) {
const ConfigOptionInt* extruder_opt = dynamic_cast<const ConfigOptionInt*>(mo->config.option("extruder")); const ConfigOptionInt* extruder_opt = dynamic_cast<const ConfigOptionInt*>(mo->config.option("extruder"));
int extruder_id = 0; int extruder_id = 0;
+1818 -10
View File
File diff suppressed because it is too large Load Diff
+34 -3
View File
@@ -81,17 +81,23 @@ public:
const Vec3d plate_origin, const Vec3d plate_origin,
const std::vector<WipeTower::ToolChangeResult> &priming, const std::vector<WipeTower::ToolChangeResult> &priming,
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes, 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 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_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_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_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_wipe_tower_rotation(float(print_config.wipe_tower_rotation_angle)),
m_priming(priming), m_priming(priming),
m_tool_changes(tool_changes), 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_final_purge(final_purge),
m_layer_idx(-1), m_layer_idx(-1),
m_tool_change_idx(0), 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_plate_origin(plate_origin),
m_single_extruder_multi_material(print_config.single_extruder_multi_material), m_single_extruder_multi_material(print_config.single_extruder_multi_material),
m_enable_timelapse_print(print_config.timelapse_type.value == TimelapseType::tlSmooth), m_enable_timelapse_print(print_config.timelapse_type.value == TimelapseType::tlSmooth),
@@ -108,8 +114,18 @@ public:
} }
std::string prime(GCode &gcodegen); std::string prime(GCode &gcodegen);
void next_layer() { ++ m_layer_idx; m_tool_change_idx = 0; } void next_layer() {
std::string tool_change(GCode &gcodegen, int extruder_id, bool finish_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); bool is_empty_wipe_tower_gcode(GCode &gcodegen, int extruder_id, bool finish_layer);
std::string finalize(GCode &gcodegen); std::string finalize(GCode &gcodegen);
std::vector<float> used_filament_length() const; std::vector<float> used_filament_length() const;
@@ -140,10 +156,15 @@ private:
// Reference to cached values at the Printer class. // Reference to cached values at the Printer class.
const std::vector<WipeTower::ToolChangeResult> &m_priming; 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_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; const WipeTower::ToolChangeResult &m_final_purge;
// Current layer index. // Current layer index.
int m_layer_idx; int m_layer_idx;
int m_tool_change_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; double m_last_wipe_tower_print_z;
// BBS // BBS
@@ -385,6 +406,16 @@ private:
void check_placeholder_parser_failed(); void check_placeholder_parser_failed();
size_t get_extruder_id(unsigned int filament_id) const; 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 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; } 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; } bool last_pos_defined() const { return m_last_pos_defined; }
+333 -35
View File
@@ -20,6 +20,7 @@
#include <cassert> #include <cassert>
#include <limits> #include <limits>
#include <algorithm> #include <algorithm>
#include <cmath>
#include <unordered_map> #include <unordered_map>
#include <libslic3r.h> #include <libslic3r.h>
@@ -36,6 +37,137 @@ namespace Slic3r {
const static bool g_wipe_into_objects = false; const static bool g_wipe_into_objects = false;
constexpr double similar_color_threshold_de2000 = 20.0; 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 &region, ExtrusionRole role)
{
return role == erSolidInfill && std::abs(region.config().sparse_infill_density.value - 100.) < EPSILON;
}
bool use_base_infill_filament_impl(const LayerTools &layer_tools, const PrintRegion &region)
{
const PrintRegionConfig &config = region.config();
// 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 &region)
{
return use_base_infill_filament_impl(layer_tools, region) ? region.config().wall_filament.value : region.config().sparse_infill_filament.value;
}
unsigned int grouped_manual_pattern_mixed_filament_id_for_layer(const LayerTools& layer_tools,
unsigned int configured_filament_id_1based)
{
if (layer_tools.mixed_mgr == nullptr || layer_tools.num_physical == 0)
return 0;
if (has_grouped_manual_pattern(layer_tools.mixed_mgr, layer_tools.num_physical, configured_filament_id_1based))
return configured_filament_id_1based;
return 0;
}
unsigned int grouped_manual_pattern_infill_filament_1based(const LayerTools& layer_tools,
const PrintRegion& region,
unsigned int configured_filament_id_1based)
{
const unsigned int grouped_id =
grouped_manual_pattern_mixed_filament_id_for_layer(layer_tools, configured_filament_id_1based);
if (grouped_id == 0)
return 0;
const int innermost_perimeter_index = std::max(0, region.config().wall_loops.value - 1);
return layer_tools.mixed_mgr->resolve_perimeter(grouped_id,
layer_tools.num_physical,
layer_tools.layer_index,
innermost_perimeter_index,
float(layer_tools.print_z),
float(layer_tools.layer_height));
}
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) 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; std::set<int> target_filaments;
@@ -79,23 +211,50 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
return true; 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. // Return a zero based extruder from the region, or extruder_override if overriden.
unsigned int LayerTools::wall_filament(const PrintRegion &region) const unsigned int LayerTools::wall_filament(const PrintRegion &region) const
{ {
assert(region.config().wall_filament.value > 0); assert(region.config().wall_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().wall_filament.value : this->extruder_override) - 1; unsigned int id_1based = (this->extruder_override == 0)
? region.config().wall_filament.value
: this->extruder_override;
return resolve_mixed_1based(id_1based) - 1;
} }
unsigned int LayerTools::sparse_infill_filament(const PrintRegion &region) const unsigned int LayerTools::sparse_infill_filament(const PrintRegion &region) const
{ {
assert(region.config().sparse_infill_filament.value > 0); assert(region.config().sparse_infill_filament.value > 0);
return ((this->extruder_override == 0) ? region.config().sparse_infill_filament.value : this->extruder_override) - 1; unsigned int id_1based = (this->extruder_override == 0)
? sparse_infill_filament_id_1based_impl(*this, region)
: this->extruder_override;
const unsigned int grouped = grouped_manual_pattern_infill_filament_1based(*this, region, id_1based);
return ((grouped != 0) ? grouped : resolve_mixed_1based(id_1based)) - 1;
} }
unsigned int LayerTools::solid_infill_filament(const PrintRegion &region) const unsigned int LayerTools::solid_infill_filament(const PrintRegion &region) const
{ {
assert(region.config().solid_infill_filament.value > 0); 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. // 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().wall_filament.value > 0);
assert(region.config().sparse_infill_filament.value > 0); assert(region.config().sparse_infill_filament.value > 0);
assert(region.config().solid_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_infill()) {
if (extrusions.has_solid_infill()) const ExtrusionRole role = extrusions.entities.empty() ? erNone : extrusions.entities.front()->role();
extruder = region.config().solid_infill_filament; if (internal_solid_infill_uses_sparse_filament(region, role))
else return sparse_infill_filament(region);
extruder = region.config().sparse_infill_filament; return is_solid_infill(role) ? solid_infill_filament(region) : sparse_infill_filament(region);
} else }
extruder = region.config().wall_filament.value; return wall_filament(region);
} else }
extruder = this->extruder_override;
return (extruder == 0) ? 0 : extruder - 1; unsigned int LayerTools::sparse_infill_filament_id_1based(const PrintRegion &region) const
{
return sparse_infill_filament_id_1based_impl(*this, region);
}
unsigned int LayerTools::infill_filament_id_1based(const PrintRegion &region) const
{
// Default role: erInternalInfill routes through sparse path.
return sparse_infill_filament_id_1based_impl(*this, region);
}
bool LayerTools::use_base_infill_filament(const PrintRegion &region) const
{
return use_base_infill_filament_impl(*this, region);
} }
static double calc_max_layer_height(const PrintConfig &config, double max_object_layer_height) 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 // Reorder the extruders of first layer
{ {
LayerTools& lt = m_layer_tools[0]; LayerTools& lt = m_layer_tools[0];
if (!lt.preserve_extruder_order) {
std::vector<unsigned int> layer0_extruders = lt.extruders; std::vector<unsigned int> layer0_extruders = lt.extruders;
lt.extruders.clear(); lt.extruders.clear();
for (unsigned int extruder_id : tool_order_layer0) { 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]); lt.extruders.push_back(tool_order_layer0[0]);
} }
} }
}
int last_extruder_id = m_layer_tools[0].extruders.back(); int last_extruder_id = m_layer_tools[0].extruders.back();
for (int i = 1; i < m_layer_tools.size(); i++) { 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) if (lt.extruders.front() == 0)
// Pop the "don't care" extruder, the "don't care" region will be merged with the next one. // Pop the "don't care" extruder, the "don't care" region will be merged with the next one.
lt.extruders.erase(lt.extruders.begin()); 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. // Reorder the extruders to start with the last one.
for (size_t i = 1; i < lt.extruders.size(); ++i) for (size_t i = 1; i < lt.extruders.size(); ++i)
if (lt.extruders[i] == last_extruder_id) { 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) if (lt.extruders.front() == 0)
// Pop the "don't care" extruder, the "don't care" region will be merged with the next one. // Pop the "don't care" extruder, the "don't care" region will be merged with the next one.
lt.extruders.erase(lt.extruders.begin()); 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. // Reorder the extruders to start with the last one.
for (size_t i = 1; i < lt.extruders.size(); ++ i) for (size_t i = 1; i < lt.extruders.size(); ++ i)
if (lt.extruders[i] == last_extruder_id) { 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_full_config = &object.print()->full_print_config();
m_print_object_ptr = &object; m_print_object_ptr = &object;
m_print = const_cast<Print*>(object.print()); 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()) if (object.layers().empty())
return; 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_full_config = &print.full_print_config();
m_print = const_cast<Print *>(&print); // for update the context of print m_print = const_cast<Print *>(&print); // for update the context of print
m_print_config_ptr = &print.config(); 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. // Initialize the print layers for all objects and all layers.
coordf_t max_layer_height = 0.; 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); 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) { 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"); const ConfigOptionInts* first_layer_print_sequence_op = config->option<ConfigOptionInts>("first_layer_print_sequence");
if (first_layer_print_sequence_op) { 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. // 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) void ToolOrdering::collect_extruders(const PrintObject &object, const std::vector<std::pair<double, unsigned int>> &per_layer_extruder_switches)
{ {
// Propagate mixed-filament context to all LayerTools entries.
for (LayerTools &lt : m_layer_tools) {
lt.mixed_mgr = m_mixed_mgr;
lt.num_physical = m_num_physical;
lt.mixed_layer_height_a = m_mixed_layer_height_a;
lt.mixed_layer_height_b = m_mixed_layer_height_b;
lt.mixed_base_layer_height = m_mixed_base_layer_height;
}
// Extruder overrides are ordered by print_z. // Extruder overrides are ordered by print_z.
std::vector<std::pair<double, unsigned int>>::const_iterator it_per_layer_extruder_override; std::vector<std::pair<double, unsigned int>>::const_iterator it_per_layer_extruder_override;
it_per_layer_extruder_override = per_layer_extruder_switches.begin(); 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. // Collect the object extruders.
for (auto layer : object.layers()) { for (auto layer : object.layers()) {
LayerTools &layer_tools = this->tools_for_layer(layer->print_z); 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 // 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) 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){ if (something_nonoverriddable){
layer_tools.extruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override); const unsigned int configured_wall = (extruder_override == 0)
if (layerCount == 0) { ? region.config().wall_filament.value
firstLayerExtruders.emplace_back((extruder_override == 0) ? region.config().wall_filament.value : extruder_override); : 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 (something_nonoverriddable || !m_print_config_ptr) {
if (extruder_override == 0) { if (extruder_override == 0) {
if (has_solid_infill) 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) if (has_infill)
layer_tools.extruders.emplace_back(region.config().sparse_infill_filament); layer_tools.extruders.emplace_back(layer_tools.sparse_infill_filament(region) + 1);
} else if (has_solid_infill || has_infill) } else if (has_solid_infill || has_infill) {
layer_tools.extruders.emplace_back(extruder_override); layer_tools.extruders.emplace_back(resolve_mixed(extruder_override,
layerCount,
float(layer->print_z),
float(layer->height)));
}
} }
if (has_solid_infill || has_infill) if (has_solid_infill || has_infill)
layer_tools.has_object = true; layer_tools.has_object = true;
@@ -730,18 +991,23 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
// Collect the support extruders. // Collect the support extruders.
for (auto support_layer : object.support_layers()) { for (auto support_layer : object.support_layers()) {
LayerTools &layer_tools = this->tools_for_layer(support_layer->print_z); 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(); ExtrusionRole role = support_layer->support_fills.role();
bool has_support = false; bool has_support = role == erMixed || role == erSupportMaterial || role == erSupportTransition;
bool has_interface = false; bool has_interface = role == erMixed || role == erSupportMaterialInterface;
for (const ExtrusionEntity *ee : support_layer->support_fills.entities) {
ExtrusionRole er = ee->role(); unsigned int extruder_support = resolve_mixed(object.config().support_filament.value,
if (er == erSupportMaterial || er == erSupportTransition) has_support = true; layer_tools.layer_index,
if (er == erSupportMaterialInterface) has_interface = true; float(support_layer->print_z),
if (has_support && has_interface) break; float(support_layer->height));
} unsigned int extruder_interface = resolve_mixed(object.config().support_interface_filament.value,
unsigned int extruder_support = object.config().support_filament.value; layer_tools.layer_index,
unsigned int extruder_interface = object.config().support_interface_filament.value; float(support_layer->print_z),
float(support_layer->height));
if (has_support) { 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) if (extruder_support > 0 || !has_interface || extruder_interface == 0 || layer_tools.has_object)
layer_tools.extruders.push_back(extruder_support); layer_tools.extruders.push_back(extruder_support);
else { else {
@@ -750,7 +1016,6 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
std::vector<float> flush_matrix( 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()))); 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); 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; std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < number_of_extruders; ++i) 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)); 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) { 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); 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) // 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 &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); auto curr_flush_info = calc_filament_change_info_by_toolorder(print_config, filament_maps, nozzle_flush_mtx, filament_sequences);
if (nozzle_nums <= 1) if (nozzle_nums <= 1)
m_stats_by_single_extruder = curr_flush_info; 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]); 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. // 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) 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; 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 } // namespace Slic3r
+52
View File
@@ -4,6 +4,7 @@
#define slic3r_ToolOrdering_hpp_ #define slic3r_ToolOrdering_hpp_
#include "../libslic3r.h" #include "../libslic3r.h"
#include "../MixedFilament.hpp"
#include <utility> #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. // Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion &region) const; unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion &region) const;
// Stub helpers for per-layer infill override (Task 14). Each returns
// the configured 1-based filament ID without per-layer logic until Task 14 lands.
unsigned int sparse_infill_filament_id_1based(const PrintRegion &region) const;
unsigned int infill_filament_id_1based(const PrintRegion &region) const;
bool use_base_infill_filament(const PrintRegion &region) const;
coordf_t print_z = 0.; coordf_t print_z = 0.;
bool has_object = false; bool has_object = false;
bool has_support = false; bool has_support = false;
// Zero based extruder IDs, ordered to minimize tool switches. // Zero based extruder IDs, ordered to minimize tool switches.
std::vector<unsigned int> extruders; 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 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. // If not overriden, it is set to 0.
unsigned int extruder_override = 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? // 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. // Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed.
bool has_skirt = false; bool has_skirt = false;
@@ -172,6 +203,10 @@ public:
return m_wiping_extrusions; 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: private:
// This object holds list of extrusion that will be used for extruder wiping // This object holds list of extrusion that will be used for extruder wiping
WipingExtrusions m_wiping_extrusions; WipingExtrusions m_wiping_extrusions;
@@ -254,6 +289,14 @@ public:
bool has_non_support_filament(const PrintConfig &config); 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: private:
void initialize_layers(std::vector<coordf_t> &zs); 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); 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 mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height);
void collect_extruder_statistics(bool prime_multi_material); void collect_extruder_statistics(bool prime_multi_material);
void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer); 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 // BBS
std::vector<unsigned int> generate_first_layer_tool_order(const Print& print); 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 PrintConfig* m_print_config_ptr = nullptr;
const PrintObject* m_print_object_ptr = nullptr; const PrintObject* m_print_object_ptr = nullptr;
Print* m_print; 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; bool m_sorted = false;
FilamentChangeStats m_stats_by_single_extruder; FilamentChangeStats m_stats_by_single_extruder;
+352 -59
View File
@@ -6,6 +6,7 @@
#include <vector> #include <vector>
#include <numeric> #include <numeric>
#include <memory> #include <memory>
#include <limits>
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
@@ -19,6 +20,7 @@
#include "Fill/FillRectilinear.hpp" #include "Fill/FillRectilinear.hpp"
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r 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 WIPE_TOWER_RESOLUTION = 0.1;
static constexpr double WT_SIMPLIFY_TOLERANCE_SCALED = 0.001f / SCALING_FACTOR_INTERNAL; static constexpr double WT_SIMPLIFY_TOLERANCE_SCALED = 0.001f / SCALING_FACTOR_INTERNAL;
static constexpr int arc_fit_size = 20; 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) #define SCALED_WIPE_TOWER_RESOLUTION (WIPE_TOWER_RESOLUTION / SCALING_FACTOR_INTERNAL)
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow }; 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}}; 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_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; } 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_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_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_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_y_shift(0.f),
m_z_pos(0.f), m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)), m_bridging(float(config.wipe_tower_bridging)),
@@ -1515,49 +1524,50 @@ std::vector<WipeTower::ToolChangeResult> WipeTower2::prime(
return results; 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_area = 0.f;
float wipe_volume = 0.f; float wipe_volume = 0.f;
bool interface_layer = m_enable_tower_interface_features && m_current_layer_has_interface; bool interface_layer = m_enable_tower_interface_features && m_current_layer_has_interface;
// Finds this toolchange info if (tool_change != nullptr) {
if (tool != (unsigned int)(-1)) wipe_volume = tool_change->wipe_volume;
{ wipe_area = tool_change->required_depth;
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 (interface_layer && !is_no_tool_sentinel(tool) && tool < m_filpar.size()) {
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()) {
float extra_purge_length = m_filpar[tool].tower_interface_purge_length; float extra_purge_length = m_filpar[tool].tower_interface_purge_length;
if (extra_purge_length > 0.f) { if (extra_purge_length > 0.f) {
wipe_volume += extra_purge_length * m_filpar[tool].filament_area; wipe_volume += extra_purge_length * m_filpar[tool].filament_area;
} }
} }
WipeTower::box_coordinates cleaning_box( WipeTower::box_coordinates cleaning_box(Vec2f(m_perimeter_width / 2.f, m_perimeter_width / 2.f), m_wipe_tower_width - m_perimeter_width,
Vec2f(m_perimeter_width / 2.f, m_perimeter_width / 2.f), (!is_no_tool_sentinel(tool) ? wipe_area + m_depth_traversed - 0.5f * m_perimeter_width :
m_wipe_tower_width - m_perimeter_width, m_wipe_tower_depth - m_perimeter_width));
(tool != (unsigned int)(-1) ? 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); WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting);
writer.set_extrusion_flow(m_extrusion_flow) writer.set_extrusion_flow(m_extrusion_flow)
.set_z(m_z_pos) .set_z(m_z_pos)
.set_initial_tool(m_current_tool) .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" .append(";--------------------\n"
"; CP TOOLCHANGE START\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.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()) 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"); .append(";--------------------\n");
@@ -1574,8 +1584,10 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
if (m_set_extruder_trimpot) if (m_set_extruder_trimpot)
writer.set_extruder_trimpot(750); 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. // 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; 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, 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), (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 } else
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, m_filpar[m_current_tool].temperature, m_filpar[m_current_tool].temperature); 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; m_depth_traversed += wipe_area;
if (m_set_extruder_trimpot) 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()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); 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 else
sparse_beginning_y += (m_layer_info-1)->toolchanges_depth() + m_perimeter_width; sparse_beginning_y += (m_layer_info-1)->toolchanges_depth() + m_perimeter_width;
float sum_of_depths = 0.f; if (m_active_tool_change != nullptr) {
for (const auto& tch : m_layer_info->tool_changes) { // let's find this toolchange float ramming_end_y = cumulative_toolchange_depth_before(m_active_tool_change) + m_active_tool_change->ramming_depth;
if (tch.old_tool == m_current_tool) {
sum_of_depths += tch.ramming_depth;
float ramming_end_y = sum_of_depths;
ramming_end_y -= (y_step / m_extra_spacing_ramming - m_perimeter_width) / 2.f; // center of final ramming line 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) || 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 ) ) (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());
writer.extrude(xl + tch.first_wipe_line-1.f*m_perimeter_width,writer.y()); remaining -= m_active_tool_change->first_wipe_line - 1.f * m_perimeter_width;
remaining -= tch.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(writer.x(), writer.y() - dy)
.add_wipe_point(! m_left_to_right ? m_wipe_tower_width : 0.f, 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; m_left_to_right = !m_left_to_right;
}
writer.set_extrusion_flow(m_extrusion_flow); // Reset the extrusion flow. writer.set_extrusion_flow(m_extrusion_flow); // Reset the extrusion flow.
writer.change_analyzer_line_width(m_perimeter_width); 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); 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) if (m_enable_tower_interface_features && m_prev_layer_had_interface)
feedrate = std::min(feedrate, 20.f * 60.f); 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)), 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); 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? // Is there a soluble filament wiped/rammed at the next layer?
// If so, the infill should not be sparse. // If so, the infill should not be sparse.
bool solid_infill = m_layer_info+1 == m_plan.end() bool solid_infill = m_layer_info + 1 == m_plan.end() ? false : layer_has_soluble_toolchange(*(m_layer_info + 1));
? 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;
});
solid_infill |= first_layer && m_adhesion; solid_infill |= first_layer && m_adhesion;
if (solid_infill) { 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)); 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() 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) 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; m_plan[layer_index].depth = this_layer_depth;
if (this_layer_depth > m_wipe_tower_depth - m_perimeter_width) 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) { for (int i=0; i<int(m_layer_info->tool_changes.size()); ++i) {
auto& toolchange = m_layer_info->tool_changes[i]; auto& toolchange = m_layer_info->tool_changes[i];
tool_change(toolchange.new_tool); emit_planned_tool_change(&toolchange);
if (i == idx) { if (i == idx) {
float width = m_wipe_tower_width - 3*m_perimeter_width; // width we draw into 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; return tool_changes.empty() ? -1 : 0;
} }
static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first, bool WipeTower2::layer_has_soluble_toolchange(const WipeTowerInfo &layer) const
WipeTower::ToolChangeResult& second) {
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); assert(first.new_tool == second.initial_tool);
WipeTower::ToolChangeResult out = first; WipeTower::ToolChangeResult out = first;
@@ -2358,10 +2634,11 @@ static WipeTower::ToolChangeResult merge_tcr(WipeTower::ToolChangeResult& first,
return out; return out;
} }
// Processes vector m_plan and calls respective functions to generate G-code for the wipe tower.
// 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
// Resulting ToolChangeResults are appended into vector "result" // 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) void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>& result,
std::vector<std::vector<WipeTower::ToolChangeResult>>& local_z_result)
{ {
if (m_plan.empty()) if (m_plan.empty())
return; return;
@@ -2383,8 +2660,12 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
m_layer_info = m_plan.begin(); m_layer_info = m_plan.begin();
m_current_height = 0.f; 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) { 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()) { if (!layer.tool_changes.empty()) {
m_current_tool = layer.tool_changes.front().old_tool; m_current_tool = layer.tool_changes.front().old_tool;
break; break;
@@ -2400,6 +2681,14 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
for (const WipeTower2::WipeTowerInfo& layer : m_plan) for (const WipeTower2::WipeTowerInfo& layer : m_plan)
{ {
std::vector<WipeTower::ToolChangeResult> layer_result; 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); 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; 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); int idx = first_toolchange_to_nonsoluble(layer.tool_changes);
WipeTower::ToolChangeResult finish_layer_tcr; 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 (idx == -1) {
// if there is no toolchange switching to non-soluble, finish layer // If there is no nominal-layer toolchange switching to non-soluble,
// will be called at the very beginning. That's the last possibility // finish_layer still runs after any Local-Z toolchanges already planned
// where a nonsoluble tool can be. // onto this tower layer.
finish_layer_tcr = finish_layer(); finish_layer_tcr = finish_layer();
} }
for (int i = 0; i < int(layer.tool_changes.size()); ++i) { 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 if (i == idx) // finish_layer will be called after this toolchange
finish_layer_tcr = finish_layer(); 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)); 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) 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(); m_used_filament_length_until_layer.emplace_back();
+26 -2
View File
@@ -51,12 +51,17 @@ public:
// Appends into internal structure m_plan containing info about the future wipe tower // 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. // 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_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" // 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; } float get_depth() const { return m_wipe_tower_depth; }
std::vector<std::pair<float, float>> get_z_and_depth_pairs() const; 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_brim_width() const { return m_wipe_tower_brim_width_real; }
float get_wipe_tower_height() const { return m_wipe_tower_height; } float get_wipe_tower_height() const { return m_wipe_tower_height; }
// ORCA: Match WipeTower API used by Print skirt/brim planning. // 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. // Returns gcode for a toolchange and a final print head position.
// On the first layer, extrude a brim around the future wipe tower first. // On the first layer, extrude a brim around the future wipe tower first.
WipeTower::ToolChangeResult tool_change(size_t new_tool); 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. // Fill the unfilled space with a sparse infill.
// Call this method only if layer_finished() is false. // Call this method only if layer_finished() is false.
@@ -182,6 +190,8 @@ public:
}; };
private: private:
struct WipeTowerInfo;
enum wipe_shape // A fill-in direction enum wipe_shape // A fill-in direction
{ {
SHAPE_NORMAL = 1, SHAPE_NORMAL = 1,
@@ -260,6 +270,9 @@ private:
// Extruder specific parameters. // Extruder specific parameters.
std::vector<FilamentParameters> m_filpar; 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. // State of the wipe tower generator.
unsigned int m_num_layer_changes = 0; // Layer change counter for the output statistics. 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. 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 z; // z position of the layer
float height; // layer height float height; // layer height
float depth; // depth of the layer based on all layers above 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> tool_changes;
std::vector<ToolChange> local_z_tool_changes;
WipeTowerInfo(float z_par, float layer_height_par) WipeTowerInfo(float z_par, float layer_height_par)
: z{z_par}, height{layer_height_par}, depth{0} {} : 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> 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(); 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 // This sums height of all extruded layers, not counting the layers which
// will be later removed when the "no_sparse_layers" is used. // will be later removed when the "no_sparse_layers" is used.
@@ -332,6 +353,9 @@ private:
// ot -1 if there is no such toolchange. // ot -1 if there is no such toolchange.
int first_toolchange_to_nonsoluble( int first_toolchange_to_nonsoluble(
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const; 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( void toolchange_Unload(
WipeTowerWriter2 &writer, WipeTowerWriter2 &writer,
+72
View File
@@ -0,0 +1,72 @@
#ifndef slic3r_LocalZOrderOptimizer_hpp_
#define slic3r_LocalZOrderOptimizer_hpp_
#include <algorithm>
#include <numeric>
#include <vector>
namespace Slic3r {
namespace LocalZOrderOptimizer {
inline bool bucket_contains_extruder(const std::vector<unsigned int> &extruders, int extruder_id)
{
return extruder_id >= 0 &&
std::find(extruders.begin(), extruders.end(), static_cast<unsigned int>(extruder_id)) != extruders.end();
}
inline std::vector<unsigned int> order_bucket_extruders(std::vector<unsigned int> extruders,
int current_extruder,
int preferred_last_extruder = -1)
{
extruders.erase(std::unique(extruders.begin(), extruders.end()), extruders.end());
if (extruders.empty())
return extruders;
if (current_extruder >= 0) {
auto current_it = std::find(extruders.begin(), extruders.end(), static_cast<unsigned int>(current_extruder));
if (current_it != extruders.end())
std::rotate(extruders.begin(), current_it, extruders.end());
}
if (preferred_last_extruder >= 0 && extruders.size() > 1 && static_cast<int>(extruders.front()) != preferred_last_extruder) {
auto preferred_it = std::find(extruders.begin() + 1, extruders.end(), static_cast<unsigned int>(preferred_last_extruder));
if (preferred_it != extruders.end())
std::rotate(preferred_it, preferred_it + 1, extruders.end());
}
return extruders;
}
inline std::vector<size_t> order_pass_group(const std::vector<std::vector<unsigned int>> &group_extruders, int current_extruder)
{
std::vector<size_t> remaining(group_extruders.size());
std::iota(remaining.begin(), remaining.end(), size_t(0));
std::vector<size_t> ordered;
ordered.reserve(group_extruders.size());
int active_extruder = current_extruder;
while (!remaining.empty()) {
auto next_it = std::find_if(remaining.begin(), remaining.end(), [&](size_t idx) {
return bucket_contains_extruder(group_extruders[idx], active_extruder);
});
if (next_it == remaining.end())
next_it = remaining.begin();
const size_t next_idx = *next_it;
ordered.push_back(next_idx);
const std::vector<unsigned int> ordered_bucket = order_bucket_extruders(group_extruders[next_idx], active_extruder);
if (!ordered_bucket.empty())
active_extruder = static_cast<int>(ordered_bucket.back());
remaining.erase(next_it);
}
return ordered;
}
} // namespace LocalZOrderOptimizer
} // namespace Slic3r
#endif
File diff suppressed because it is too large Load Diff
+317
View File
@@ -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
View File
@@ -2575,15 +2575,26 @@ void ModelVolume::update_extruder_count(size_t extruder_count)
} }
} }
void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id) void ModelVolume::update_extruder_count_when_delete_filament(size_t extruder_count, size_t filament_id, int replace_filament_id,
const std::vector<unsigned char>& filament_is_mixed)
{ {
std::vector<int> used_extruders = get_extruders(); std::vector<int> used_extruders = get_extruders();
for (int extruder_id : used_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)); mmu_segmentation_facets.set_enforcer_block_type_limit(*this, (EnforcerBlockerType)(extruder_count), (EnforcerBlockerType)(filament_id), (EnforcerBlockerType)(replace_filament_id));
break; 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) void ModelVolume::center_geometry_after_creation(bool update_source_offset)
@@ -3521,6 +3532,15 @@ bool FacetsAnnotation::set(const TriangleSelector& selector)
return false; 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() void FacetsAnnotation::reset()
{ {
m_data.triangles_to_split.clear(); m_data.triangles_to_split.clear();
+5 -1
View File
@@ -742,6 +742,9 @@ public:
EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE); EnforcerBlockerType replace_filament = EnforcerBlockerType::NONE);
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const; indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
bool has_facets(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(); } bool empty() const { return m_data.triangles_to_split.empty(); }
// Following method clears the config and increases its timestamp, so the deleted // Following method clears the config and increases its timestamp, so the deleted
@@ -925,7 +928,8 @@ public:
// BBS // BBS
std::vector<int> get_extruders() const; std::vector<int> get_extruders() const;
void update_extruder_count(size_t extruder_count); 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. // Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one. // Return the number of volumes created from this one.
+12 -8
View File
@@ -1829,15 +1829,14 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
{ {
const size_t num_layers = segmented_regions.size(); const size_t num_layers = segmented_regions.size();
std::vector<std::vector<ExPolygons>> segmented_regions_merged(num_layers); 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()); 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"; 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) { 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) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states); 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 = 0; extruder_id < num_facets_states; ++extruder_id) {
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
throw_on_cancel_callback(); throw_on_cancel_callback();
if (!segmented_regions[layer_idx][extruder_id].empty()) { if (!segmented_regions[layer_idx][extruder_id].empty()) {
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id]; 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()) { 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(); bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id].empty();
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]); 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. // Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
if (!was_top_and_bottom_empty) 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 // 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) { 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 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 float interlocking_depth = float(print_object.config().mmu_segmented_region_interlocking_depth.value);
const bool interlocking_beam = print_object.config().interlocking_beam.value; const bool interlocking_beam = print_object.config().interlocking_beam.value;
+20
View File
@@ -1264,6 +1264,26 @@ static std::vector<std::string> s_Preset_print_options{
"zaa_dont_alternate_fill_direction", "zaa_dont_alternate_fill_direction",
"zaa_min_z", "zaa_min_z",
"ironing_expansion", "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", 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
View File
@@ -10,9 +10,11 @@
#include "libslic3r_version.h" #include "libslic3r_version.h"
#include <algorithm> #include <algorithm>
#include <map>
#include <mutex> #include <mutex>
#include <set> #include <set>
#include <fstream> #include <fstream>
#include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/algorithm/clamp.hpp> #include <boost/algorithm/clamp.hpp>
@@ -53,7 +55,9 @@ static std::vector<std::string> s_project_options {
"flush_multiplier", "flush_multiplier",
"nozzle_volume_type", "nozzle_volume_type",
"filament_map_mode", "filament_map_mode",
"filament_map" "filament_map",
// FullSpectrum: mixed filament definitions string (virtual slot configuration)
"mixed_filament_definitions"
}; };
//Orca: add custom as default //Orca: add custom as default
@@ -2682,6 +2686,7 @@ void PresetBundle::update_selections(AppConfig &config)
// exist. // exist.
this->update_compatible(PresetSelectCompatibleType::Always); this->update_compatible(PresetSelectCompatibleType::Always);
this->update_multi_material_filament_presets(); this->update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
std::string first_visible_filament_name; std::string first_visible_filament_name;
for (auto & fp : filament_presets) { 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()); 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. // 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, // 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 // as the application may have been closed with an active "external" preset, which does not
// exist. // exist.
this->update_compatible(PresetSelectCompatibleType::Always); this->update_compatible(PresetSelectCompatibleType::Always);
this->update_multi_material_filament_presets(); this->update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
if (initial_printer != nullptr && (preferred_printer == nullptr || initial_printer == preferred_printer)) { 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. // 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); 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 // BBS
//config.set("presets", "sla_print", sla_prints.get_selected_preset_name()); //config.set("presets", "sla_print", sla_prints.get_selected_preset_name());
//config.set("presets", "sla_material", sla_materials.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(); update_multi_material_filament_presets();
sync_mixed_filaments_from_config();
} }
void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) 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(); 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) 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(filament_color_type->values);
erase_or_resize(ams_multi_color_filment); 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); 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) 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; 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_presets;
std::vector<std::string> ams_filament_colors; std::vector<std::string> ams_filament_colors;
std::vector<std::string> ams_filament_color_types; 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()) if (ams_filament_presets.empty())
return 0; 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"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "get filament_colour and from config";
ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour"); ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type"); 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()) if (support_interface_filament_opt->value > filament_color_type->values.size())
support_interface_filament_opt->value = 0; 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 ams_multi_color_filment
update_filament_multi_color(); update_filament_multi_color();
update_multi_material_filament_presets(); 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 //BBS: add logic for settings check between different system presets
out.erase("different_settings_to_system"); out.erase("different_settings_to_system");
static const char* keys[] = {"support_filament", "support_interface_filament", "wipe_tower_filament"}; const size_t num_total_filaments = this->mixed_filaments.total_filaments(num_filaments);
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); ++ i) {
std::string key = std::string(keys[i]); 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)); auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false));
assert(opt != nullptr); assert(opt != nullptr);
opt->value = boost::algorithm::clamp<int>(opt->value, 0, int(num_filaments)); 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"}; static const char* feature_keys[] = {"wall_filament", "sparse_infill_filament", "solid_infill_filament", "wipe_tower_filament"};
for (size_t i = 0; i < sizeof(keys_with_default) / sizeof(keys_with_default[0]); ++ i) { for (size_t i = 0; i < sizeof(feature_keys) / sizeof(feature_keys[0]); ++ i) {
std::string key = std::string(keys_with_default[i]); std::string key = std::string(feature_keys[i]);
auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false)); auto *opt = dynamic_cast<ConfigOptionInt*>(out.option(key, false));
assert(opt != nullptr); 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; opt->value = 0;
} }
out.option<ConfigOptionString >("print_settings_id", true)->value = this->prints.get_selected_preset_name(); 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_compatible(PresetSelectCompatibleType::Never);
this->update_multi_material_filament_presets(); 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 //BBS
//const std::string &physical_printer = config.option<ConfigOptionString>("physical_printer_settings_id", true)->value; //const std::string &physical_printer = config.option<ConfigOptionString>("physical_printer_settings_id", true)->value;
const std::string physical_printer; const std::string physical_printer;
@@ -5116,7 +5181,7 @@ void PresetBundle::on_extruders_count_changed(int extruders_count)
extruder_ams_counts.resize(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) if (printers.get_edited_preset().printer_technology() != ptFFF)
return; return;
@@ -5127,6 +5192,13 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
// Verify and select the filament presets. // Verify and select the filament presets.
size_t num_filaments = this->filament_presets.size(); 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")); auto* nozzle_diameter = static_cast<const ConfigOptionFloats*>(printers.get_edited_preset().config.option("nozzle_diameter"));
size_t num_extruders = nozzle_diameter->values.size(); size_t num_extruders = nozzle_diameter->values.size();
if (num_extruders > num_filaments) { // Verify validity of the current filament presets. 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()); this->filament_presets.back());
num_filaments = this->filament_presets.size(); num_filaments = this->filament_presets.size();
} }
if (to_delete_filament_id == -1)
if (!deleting_filament)
to_delete_filament_id = num_filaments; 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): // 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; 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) void PresetBundle::update_compatible(PresetSelectCompatibleType select_other_print_if_incompatible, PresetSelectCompatibleType select_other_filament_if_incompatible)
+42 -1
View File
@@ -4,6 +4,7 @@
#include "Preset.hpp" #include "Preset.hpp"
#include "AppConfig.hpp" #include "AppConfig.hpp"
#include "enum_bitmask.hpp" #include "enum_bitmask.hpp"
#include "MixedFilament.hpp"
#include <memory> #include <memory>
#include <shared_mutex> #include <shared_mutex>
@@ -289,6 +290,19 @@ public:
//BBS: check whether this is the only edited filament //BBS: check whether this is the only edited filament
bool is_the_only_edited_filament(unsigned int filament_index); 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(); void reset_default_nozzle_volume_type();
std::vector<int> get_used_tpu_filaments(const std::vector<int> &used_filaments); 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::map<int, DynamicPrintConfig> filament_ams_list;
std::vector<std::vector<std::string>> ams_multi_color_filment; 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; std::vector<std::map<int, int>> extruder_ams_counts;
// Calibrate // Calibrate
@@ -436,7 +454,23 @@ public:
// Read out the number of extruders from an active printer preset, // Read out the number of extruders from an active printer preset,
// update size and content of filament_presets. // 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); void on_extruders_count_changed(int extruder_count);
@@ -493,6 +527,12 @@ private:
void update_filament_multi_color(); void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles. // Update renamed_from and alias maps of system profiles.
void update_system_maps(); 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, // Set the is_visible flag for filaments and sla materials,
// apply defaults based on enabled printers when no filaments/materials are installed. // apply defaults based on enabled printers when no filaments/materials are installed.
@@ -513,6 +553,7 @@ private:
bool validation_mode = false; bool validation_mode = false;
std::string vendor_to_validate = ""; std::string vendor_to_validate = "";
int m_errors = 0; int m_errors = 0;
std::vector<unsigned int> m_last_filament_id_remap;
// Helper function: save preset to bundle directory with common logic // Helper function: save preset to bundle directory with common logic
bool save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection, bool save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection,
+584 -6
View File
@@ -8,6 +8,7 @@
#include "Flow.hpp" #include "Flow.hpp"
#include "Geometry/ConvexHull.hpp" #include "Geometry/ConvexHull.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "LocalZOrderOptimizer.hpp"
#include "ShortestPath.hpp" #include "ShortestPath.hpp"
#include "Thread.hpp" #include "Thread.hpp"
#include "Time.hpp" #include "Time.hpp"
@@ -17,6 +18,7 @@
#include "Utils.hpp" #include "Utils.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "MaterialType.hpp" #include "MaterialType.hpp"
#include "MixedFilament.hpp"
#include "Model.hpp" #include "Model.hpp"
#include "format.hpp" #include "format.hpp"
#include <float.h> #include <float.h>
@@ -54,6 +56,489 @@ template class PrintState<PrintObjectStep, posCount>;
PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {} PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {}
PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(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 //BBS
// ORCA: Now this is a parameter // ORCA: Now this is a parameter
//float Print::min_skirt_length = 0; //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 == "filament_shrinkage_compensation_z"
|| opt_key == "resolution" || opt_key == "resolution"
|| opt_key == "precise_z_height" || 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: // 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. // 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. // 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>()); layer_height_profiles.assign(m_objects.size(), std::vector<coordf_t>());
std::vector<coordf_t> &profile = layer_height_profiles[print_object_idx]; std::vector<coordf_t> &profile = layer_height_profiles[print_object_idx];
if (profile.empty()) 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; 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; 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) { 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); 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) { for (size_t idx = 0; idx < tool_ordering.layer_tools().size(); ++idx) {
auto& layer_filament = tool_ordering.layer_tools()[idx].extruders; auto layer_filament = tool_ordering.layer_tools()[idx].extruders;
all_filaments.emplace_back(layer_filament); 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) 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); sort_remove_duplicates(first_layer_used_filaments);
@@ -3419,6 +3936,8 @@ void Print::_make_wipe_tower()
// Initialize the wipe tower. // Initialize the wipe tower.
WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes, WipeTower2 wipe_tower(m_config, m_default_region_config, m_plate_index, m_origin, wipe_volumes,
m_wipe_tower_data.tool_ordering.first_extruder()); 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_retract();
// wipe_tower.set_zhop(); // 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 for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
if (!layer_tools.has_wipe_tower) if (!layer_tools.has_wipe_tower)
continue; 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(); 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, wipe_tower.plan_toolchange((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, current_extruder_id,
current_extruder_id, false); 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 != if ((first_layer && extruder_id == m_wipe_tower_data.tool_ordering.all_extruders().back()) || extruder_id !=
current_extruder_id) { current_extruder_id) {
float volume_to_wipe = m_config.prime_volume; 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); 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) if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0)
break; break;
} }
@@ -3472,13 +4045,18 @@ void Print::_make_wipe_tower()
// Generate the wipe tower layers. // Generate the wipe tower layers.
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); 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.depth = wipe_tower.get_depth();
m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); 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.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height();
m_wipe_tower_data.bbx = wipe_tower.get_bbx(); m_wipe_tower_data.bbx = wipe_tower.get_bbx();
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset(); 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. // Unload the current filament over the purge tower.
coordf_t layer_height = m_objects.front()->config().layer_height.value; coordf_t layer_height = m_objects.front()->config().layer_height.value;
+57 -1
View File
@@ -4,6 +4,7 @@
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "MixedFilament.hpp"
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp" #include "ExtrusionEntityCollection.hpp"
@@ -54,6 +55,35 @@ struct groupedVolumeSlices
ExPolygons slices; 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 { enum SupportNecessaryType {
NoNeedSupp=0, NoNeedSupp=0,
SharpTail, SharpTail,
@@ -400,6 +430,19 @@ public:
std::shared_ptr<TreeSupportData> alloc_tree_support_preview_cache(); std::shared_ptr<TreeSupportData> alloc_tree_support_preview_cache();
void clear_tree_support_preview_cache() { m_tree_support_preview_cache.reset(); } 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(); } size_t support_layer_count() const { return m_support_layers.size(); }
void clear_support_layers(); void clear_support_layers();
SupportLayer* get_support_layer(int idx) { return idx<m_support_layers.size()? m_support_layers[idx]:nullptr; } 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. // 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. // 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, // Collect the slicing parameters, to be used by variable layer thickness algorithm,
// by the interactive layer height editor and by the printing process itself. // by the interactive layer height editor and by the printing process itself.
@@ -557,6 +603,8 @@ private:
SlicingParameters m_slicing_params; SlicingParameters m_slicing_params;
LayerPtrs m_layers; LayerPtrs m_layers;
SupportLayerPtrs m_support_layers; SupportLayerPtrs m_support_layers;
std::vector<LocalZInterval> m_local_z_intervals;
std::vector<SubLayerPlan> m_local_z_sublayer_plan;
// BBS // BBS
std::shared_ptr<TreeSupportData> m_tree_support_preview_cache; std::shared_ptr<TreeSupportData> m_tree_support_preview_cache;
@@ -752,6 +800,7 @@ struct WipeTowerData
// Cache of tool changes per print layer. // Cache of tool changes per print layer.
std::unique_ptr<std::vector<WipeTower::ToolChangeResult>> priming; std::unique_ptr<std::vector<WipeTower::ToolChangeResult>> priming;
std::vector<std::vector<WipeTower::ToolChangeResult>> tool_changes; 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::unique_ptr<WipeTower::ToolChangeResult> final_purge;
std::vector<float> used_filament; std::vector<float> used_filament;
int number_of_toolchanges; int number_of_toolchanges;
@@ -764,9 +813,12 @@ struct WipeTowerData
BoundingBoxf bbx;//including brim BoundingBoxf bbx;//including brim
Vec2f rib_offset; Vec2f rib_offset;
std::optional<WipeTowerMeshData> wipe_tower_mesh_data;//added 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() { void clear() {
priming.reset(nullptr); priming.reset(nullptr);
tool_changes.clear(); tool_changes.clear();
local_z_tool_changes.clear();
final_purge.reset(nullptr); final_purge.reset(nullptr);
used_filament.clear(); used_filament.clear();
number_of_toolchanges = -1; number_of_toolchanges = -1;
@@ -774,6 +826,7 @@ struct WipeTowerData
brim_width = 0.f; brim_width = 0.f;
rib_offset = Vec2f::Zero(); rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt; 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); 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; void auto_assign_extruders(ModelObject* model_object) const;
const PrintConfig& config() const { return m_config; } 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 PrintObjectConfig& default_object_config() const { return m_default_object_config; }
const PrintRegionConfig& default_region_config() const { return m_default_region_config; } const PrintRegionConfig& default_region_config() const { return m_default_region_config; }
ConstPrintObjectPtrsAdaptor objects() const { return ConstPrintObjectPtrsAdaptor(&m_objects); } ConstPrintObjectPtrsAdaptor objects() const { return ConstPrintObjectPtrsAdaptor(&m_objects); }
@@ -1134,6 +1189,7 @@ private:
Polygons first_layer_islands() const; Polygons first_layer_islands() const;
PrintConfig m_config; PrintConfig m_config;
MixedFilamentManager m_mixed_filament_mgr;
PrintObjectConfig m_default_object_config; PrintObjectConfig m_default_object_config;
PrintRegionConfig m_default_region_config; PrintRegionConfig m_default_region_config;
PrintObjectPtrs m_objects; PrintObjectPtrs m_objects;
+202 -9
View File
@@ -1104,6 +1104,57 @@ static PrintObjectRegions* generate_print_object_regions(
return out.release(); 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) Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config, bool extruder_applied)
{ {
#ifdef _DEBUG #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("print_settings_id", true);
new_full_config.option("filament_settings_id", true); new_full_config.option("filament_settings_id", true);
new_full_config.option("printer_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 // BBS
std::vector <unsigned int> used_filaments = this->extruders(true); 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; ModelObjectStatusDB model_object_status_db;
// 1) Synchronize model objects. // 1) Synchronize model objects.
@@ -1468,7 +1654,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
if (object_config_changed) if (object_config_changed)
model_object.config.assign_config(model_object_new.config); model_object.config.assign_config(model_object_new.config);
if (! object_diff.empty() || object_config_changed || num_extruders_changed ) { 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)) { 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); t_config_option_keys diff = print_object_status.print_object->config().diff(new_config);
if (! diff.empty()) { 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 // 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. // Producing the config for PrintObject on demand, caching it at print_object_last.
const PrintObject *print_object_last = nullptr; 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->config_apply(print_object_last ?
print_object_last->config() : 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; print_object_last = print_object;
}; };
if (old.empty()) { 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{}; std::array<bool, static_cast<size_t>(EnforcerBlockerType::ExtruderMax) + 1> used_facet_states{};
for (const ModelVolume *volume : volumes) { for (const ModelVolume *volume : volumes) {
const std::vector<bool> &volume_used_facet_states = volume->mmu_segmentation_facets.get_data().used_states; 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) 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]; 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) { 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]) if (!used_facet_states[state_idx])
painting_extruders.emplace_back(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) { 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. // 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]() { 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( verify_update_print_object_regions(
print_object.model_object()->volumes, print_object.model_object()->volumes,
m_default_region_config, m_default_region_config,
num_extruders, num_total_filaments,
*print_object_regions, *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) { [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) 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), LayerRanges(print_object.model_object()->layer_config_ranges),
m_default_region_config, m_default_region_config,
model_object_status.print_instances.front().trafo, 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), print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value),
painting_extruders, painting_extruders,
print_object.is_fuzzy_skin_painted()); print_object.is_fuzzy_skin_painted());
+223
View File
@@ -2404,6 +2404,177 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings{ "#F2754E" }); 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 // PS
def = this->add("filament_notes", coStrings); def = this->add("filament_notes", coStrings);
def->label = L("Filament notes"); def->label = L("Filament notes");
@@ -4077,6 +4248,31 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(0)); 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 = this->add("sparse_infill_line_width", coFloatOrPercent);
def->label = L("Sparse infill"); def->label = L("Sparse infill");
def->category = L("Quality"); def->category = L("Quality");
@@ -6959,6 +7155,16 @@ void PrintConfigDef::init_fff_params()
def->max = 300.; def->max = 300.;
def->set_default_value(new ConfigOptionPercent(100.)); 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 = this->add("idle_temperature", coInts);
def->label = L("Idle temperature"); def->label = L("Idle temperature");
def->tooltip = L("Nozzle temperature when the tool is currently not used in multi-tool setups. " 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; return changed_keys;
} }
+21
View File
@@ -1121,6 +1121,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercent, fuzzy_skin_ripple_offset)) ((ConfigOptionPercent, fuzzy_skin_ripple_offset))
((ConfigOptionInt, fuzzy_skin_layers_between_ripple_offset)) ((ConfigOptionInt, fuzzy_skin_layers_between_ripple_offset))
((ConfigOptionFloat, gap_infill_speed)) ((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)) ((ConfigOptionInt, sparse_infill_filament))
((ConfigOptionFloatOrPercent, sparse_infill_line_width)) ((ConfigOptionFloatOrPercent, sparse_infill_line_width))
((ConfigOptionPercent, infill_wall_overlap)) ((ConfigOptionPercent, infill_wall_overlap))
@@ -1551,6 +1554,23 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, ooze_prevention)) ((ConfigOptionBool, ooze_prevention))
((ConfigOptionString, filename_format)) ((ConfigOptionString, filename_format))
((ConfigOptionStrings, post_process)) ((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)) ((ConfigOptionString, printer_model))
((ConfigOptionFloat, resolution)) ((ConfigOptionFloat, resolution))
((ConfigOptionFloats, retraction_minimum_travel)) ((ConfigOptionFloats, retraction_minimum_travel))
@@ -1593,6 +1613,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBool, enable_tower_interface_cooldown_during_tower)) ((ConfigOptionBool, enable_tower_interface_cooldown_during_tower))
((ConfigOptionFloat, wipe_tower_bridging)) ((ConfigOptionFloat, wipe_tower_bridging))
((ConfigOptionPercent, wipe_tower_extra_flow)) ((ConfigOptionPercent, wipe_tower_extra_flow))
((ConfigOptionFloat, local_z_wipe_tower_purge_lines))
((ConfigOptionFloats, flush_volumes_matrix)) ((ConfigOptionFloats, flush_volumes_matrix))
((ConfigOptionFloats, flush_volumes_vector)) ((ConfigOptionFloats, flush_volumes_vector))
+579 -16
View File
@@ -992,6 +992,7 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
void PrintObject::clear_layers() void PrintObject::clear_layers()
{ {
this->clear_local_z_plan();
if (!m_shared_object) { if (!m_shared_object) {
for (Layer *l : m_layers) for (Layer *l : m_layers)
delete l; delete l;
@@ -1138,6 +1139,13 @@ bool PrintObject::invalidate_state_by_config_options(
steps.emplace_back(posPerimeters); steps.emplace_back(posPerimeters);
} else if ( } else if (
opt_key == "layer_height" 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_max_width"
|| opt_key == "mmu_segmented_region_interlocking_depth" || opt_key == "mmu_segmented_region_interlocking_depth"
|| opt_key == "raft_layers" || opt_key == "raft_layers"
@@ -1154,6 +1162,19 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "interlocking_depth" || opt_key == "interlocking_depth"
|| opt_key == "interlocking_boundary_avoidance" || opt_key == "interlocking_boundary_avoidance"
|| opt_key == "interlocking_beam_width") { || 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); steps.emplace_back(posSlice);
} else if ( } else if (
opt_key == "elefant_foot_compensation" 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 |= this->invalidate_steps({ posPerimeters, posPrepareInfill, posInfill, posIroning, posContouring, posSupportMaterial, posSimplifyPath, posSimplifyInfill });
invalidated |= m_print->invalidate_steps({ psSkirtBrim }); invalidated |= m_print->invalidate_steps({ psSkirtBrim });
m_slicing_params.valid = false; m_slicing_params.valid = false;
this->clear_local_z_plan();
} else if (step == posSupportMaterial) { } else if (step == posSupportMaterial) {
invalidated |= this->invalidate_steps({ posSimplifySupportPath }); invalidated |= this->invalidate_steps({ posSimplifySupportPath });
invalidated |= m_print->invalidate_steps({ psSkirtBrim }); 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(); bool result = Inherited::invalidate_all_steps() | m_print->invalidate_all_steps();
// Then reset some of the depending values. // Then reset some of the depending values.
m_slicing_params.valid = false; m_slicing_params.valid = false;
this->clear_local_z_plan();
return result; return result;
} }
@@ -3555,7 +3578,8 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
if (it->first != key_extruder) if (it->first != key_extruder)
if (ConfigOption* my_opt = out.option(it->first, false); my_opt != nullptr) { if (ConfigOption* my_opt = out.option(it->first, false); my_opt != nullptr) {
if (one_of(it->first, keys_extruders)) { 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; int extruder = static_cast<const ConfigOptionInt*>(it->second.get())->value;
if (extruder > 0) { if (extruder > 0) {
my_opt->setInt(extruder); 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; feature_overrides.solid_infill_filament = true;
else if (it->first == "wall_filament") else if (it->first == "wall_filament")
feature_overrides.wall_filament = true; 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 } else
my_opt->set(it->second.get()); my_opt->set(it->second.get());
@@ -3720,16 +3737,564 @@ std::vector<unsigned int> PrintObject::object_extruders() const
return extruders; 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; 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()) { if (layer_height_profile.empty()) {
// use the constructor because the assignement is crashing on ASAN OsX // 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 = 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; 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)) 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(); layer_height_profile.clear();
if (layer_height_profile.empty() || layer_height_profile[1] != slicing_parameters.first_object_layer_height) { if (layer_height_profile.empty() || layer_height_profile[1] != slicing_parameters.first_object_layer_height || has_dithering_ranges) {
//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, *ranges_to_use);
layer_height_profile = layer_height_profile_from_ranges(slicing_parameters, model_object.layer_config_ranges);
// The layer height profile is already compressed.
updated = true; updated = true;
} }
File diff suppressed because it is too large Load Diff
+70 -14
View File
@@ -1691,9 +1691,11 @@ void TriangleSelector::get_seed_fill_contour_recursive(const int facet_idx, cons
TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const { TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
// Each original triangle of the mesh is assigned a number encoding its state // 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 = 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 // non-leaf: xx = special side, yy = number of split sides
// These are bitwise appended and formed into one 64-bit integer. // 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; data.used_states[n] = true;
if (n >= 3) { if (n >= 3) {
assert(n <= 16); assert(n <= int(EnforcerBlockerType::ExtruderMax));
if (n <= 16) {
// Store "11" plus 4 bits of (n-3). auto push_nibble = [&data = this->data](int value) {
data.bitstream.insert(data.bitstream.end(), { true, true });
n -= 3;
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx) 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 { } else {
// Simple case, compatible with PrusaSlicer 2.3.1 and older for storing paint on supports and seams. // 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_split_sides = code & 0b11;
int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1; int num_of_children = num_of_split_sides == 0 ? 0 : num_of_split_sides + 1;
bool is_split = num_of_children != 0; 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. // Only valid if not is_split.
auto state = is_split ? EnforcerBlockerType::NONE : EnforcerBlockerType((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2); 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 // BBS
if (state == to_delete_filament) 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) if (const bool is_split = (code & 0b11) != 0; is_split)
continue; 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()); assert(facet_state < this->used_states.size());
if (facet_state >= this->used_states.size()) if (facet_state >= this->used_states.size())
continue; continue;
@@ -1946,9 +1981,17 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
auto num_children_or_state = [&next_nibble]() -> int { auto num_children_or_state = [&next_nibble]() -> int {
int code = next_nibble(); int code = next_nibble();
int num_of_split_sides = code & 0b11; int num_of_split_sides = code & 0b11;
return num_of_split_sides == 0 ? if (num_of_split_sides != 0)
((code & 0b1100) == 0b1100 ? next_nibble() + 3 : code >> 2) : return - num_of_split_sides - 1;
- 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(); int state = num_children_or_state();
@@ -2524,4 +2567,17 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting(
return target_selector.serialize(); 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 } // namespace Slic3r
+22 -2
View File
@@ -17,7 +17,8 @@ enum class EnforcerBlockerType : int8_t {
BLOCKER = 2, BLOCKER = 2,
// For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN). // For the fuzzy skin, we use just two values (NONE and FUZZY_SKIN).
FUZZY_SKIN = ENFORCER, 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, Extruder1 = ENFORCER,
Extruder2 = BLOCKER, Extruder2 = BLOCKER,
Extruder3, Extruder3,
@@ -34,7 +35,23 @@ enum class EnforcerBlockerType : int8_t {
Extruder14, Extruder14,
Extruder15, Extruder15,
Extruder16, 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 // Type alias for the state mapping array to improve code readability
@@ -391,6 +408,9 @@ public:
const indexed_triangle_set& target_its, const indexed_triangle_set& target_its,
const Transform3d& target_transform, const Transform3d& target_transform,
const std::optional<std::reference_wrapper<const TriangleSplittingData>>& existing_painting); 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: protected:
// Triangle and info about how it's split. // Triangle and info about how it's split.
+81
View File
@@ -0,0 +1,81 @@
#include "filament_mixer.h"
#include <algorithm>
#include <cmath>
#include "filament_mixer_model.h"
namespace Slic3r {
namespace {
inline float clamp01(float x)
{
return std::max(0.0f, std::min(1.0f, x));
}
inline float srgb_to_linear(float x)
{
return (x >= 0.04045f) ? std::pow((x + 0.055f) / 1.055f, 2.4f) : x / 12.92f;
}
inline float linear_to_srgb(float x)
{
return (x >= 0.0031308f) ? (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f) : (12.92f * x);
}
inline unsigned char to_u8(float x)
{
const float clamped = clamp01(x);
return static_cast<unsigned char>(clamped * 255.0f + 0.5f);
}
inline float to_f01(unsigned char x)
{
return static_cast<float>(x) / 255.0f;
}
} // namespace
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b)
{
::filament_mixer::lerp(r1, g1, b1, r2, g2, b2, t, out_r, out_g, out_b);
}
void filament_mixer_lerp_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b)
{
unsigned char ur = 0, ug = 0, ub = 0;
filament_mixer_lerp(to_u8(r1), to_u8(g1), to_u8(b1),
to_u8(r2), to_u8(g2), to_u8(b2),
t, &ur, &ug, &ub);
*out_r = to_f01(ur);
*out_g = to_f01(ug);
*out_b = to_f01(ub);
}
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b)
{
const float sr1 = linear_to_srgb(clamp01(r1));
const float sg1 = linear_to_srgb(clamp01(g1));
const float sb1 = linear_to_srgb(clamp01(b1));
const float sr2 = linear_to_srgb(clamp01(r2));
const float sg2 = linear_to_srgb(clamp01(g2));
const float sb2 = linear_to_srgb(clamp01(b2));
float out_sr = 0.0f, out_sg = 0.0f, out_sb = 0.0f;
filament_mixer_lerp_float(sr1, sg1, sb1, sr2, sg2, sb2, t, &out_sr, &out_sg, &out_sb);
*out_r = srgb_to_linear(clamp01(out_sr));
*out_g = srgb_to_linear(clamp01(out_sg));
*out_b = srgb_to_linear(clamp01(out_sb));
}
} // namespace Slic3r
+23
View File
@@ -0,0 +1,23 @@
#ifndef SLIC3R_FILAMENT_MIXER_H
#define SLIC3R_FILAMENT_MIXER_H
namespace Slic3r {
void filament_mixer_lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b);
void filament_mixer_lerp_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b);
void filament_mixer_lerp_linear_float(float r1, float g1, float b1,
float r2, float g2, float b2,
float t,
float* out_r, float* out_g, float* out_b);
} // namespace Slic3r
#endif
+819
View File
@@ -0,0 +1,819 @@
/*
* FilamentMixer Header-only C++ pigment color mixer
*
* Filament mixer implementation using a degree-4 polynomial regression
* trained to approximate Mixbox behavior (Mean Delta-E ~2.07).
* This library does not include Mixbox source code, binaries, or data files.
*
* Usage:
* #include "filament_mixer_model.h"
*
* unsigned char r, g, b;
* filament_mixer::lerp(0, 33, 133, 252, 211, 0, 0.5f, &r, &g, &b);
* // r=47, g=141, b=56 (blue + yellow → green)
*
* No dependencies beyond the C++ standard library.
*
* MIT License
*
* Copyright (c) 2026 Justin Hayes
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef FILAMENT_MIXER_H
#define FILAMENT_MIXER_H
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace filament_mixer {
namespace detail {
// BEGIN AUTO-GENERATED COEFFICIENTS
// Auto-generated by scripts/export_poly_coefficients.py
// Do not edit manually.
// Degree-4 polynomial, 330 features, 7 inputs
static const int POLY_DEGREE = 4;
static const int N_FEATURES = 330;
static const int N_INPUTS = 7;
static const int POWERS[330][7] = {
{0, 0, 0, 0, 0, 0, 0},
{1, 0, 0, 0, 0, 0, 0},
{0, 1, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 1, 0, 0},
{0, 0, 0, 0, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 1},
{2, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0},
{1, 0, 1, 0, 0, 0, 0},
{1, 0, 0, 1, 0, 0, 0},
{1, 0, 0, 0, 1, 0, 0},
{1, 0, 0, 0, 0, 1, 0},
{1, 0, 0, 0, 0, 0, 1},
{0, 2, 0, 0, 0, 0, 0},
{0, 1, 1, 0, 0, 0, 0},
{0, 1, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 1, 0, 0},
{0, 1, 0, 0, 0, 1, 0},
{0, 1, 0, 0, 0, 0, 1},
{0, 0, 2, 0, 0, 0, 0},
{0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 0, 1, 0, 0},
{0, 0, 1, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 0, 1},
{0, 0, 0, 2, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0},
{0, 0, 0, 1, 0, 1, 0},
{0, 0, 0, 1, 0, 0, 1},
{0, 0, 0, 0, 2, 0, 0},
{0, 0, 0, 0, 1, 1, 0},
{0, 0, 0, 0, 1, 0, 1},
{0, 0, 0, 0, 0, 2, 0},
{0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 2},
{3, 0, 0, 0, 0, 0, 0},
{2, 1, 0, 0, 0, 0, 0},
{2, 0, 1, 0, 0, 0, 0},
{2, 0, 0, 1, 0, 0, 0},
{2, 0, 0, 0, 1, 0, 0},
{2, 0, 0, 0, 0, 1, 0},
{2, 0, 0, 0, 0, 0, 1},
{1, 2, 0, 0, 0, 0, 0},
{1, 1, 1, 0, 0, 0, 0},
{1, 1, 0, 1, 0, 0, 0},
{1, 1, 0, 0, 1, 0, 0},
{1, 1, 0, 0, 0, 1, 0},
{1, 1, 0, 0, 0, 0, 1},
{1, 0, 2, 0, 0, 0, 0},
{1, 0, 1, 1, 0, 0, 0},
{1, 0, 1, 0, 1, 0, 0},
{1, 0, 1, 0, 0, 1, 0},
{1, 0, 1, 0, 0, 0, 1},
{1, 0, 0, 2, 0, 0, 0},
{1, 0, 0, 1, 1, 0, 0},
{1, 0, 0, 1, 0, 1, 0},
{1, 0, 0, 1, 0, 0, 1},
{1, 0, 0, 0, 2, 0, 0},
{1, 0, 0, 0, 1, 1, 0},
{1, 0, 0, 0, 1, 0, 1},
{1, 0, 0, 0, 0, 2, 0},
{1, 0, 0, 0, 0, 1, 1},
{1, 0, 0, 0, 0, 0, 2},
{0, 3, 0, 0, 0, 0, 0},
{0, 2, 1, 0, 0, 0, 0},
{0, 2, 0, 1, 0, 0, 0},
{0, 2, 0, 0, 1, 0, 0},
{0, 2, 0, 0, 0, 1, 0},
{0, 2, 0, 0, 0, 0, 1},
{0, 1, 2, 0, 0, 0, 0},
{0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 1, 0, 0},
{0, 1, 1, 0, 0, 1, 0},
{0, 1, 1, 0, 0, 0, 1},
{0, 1, 0, 2, 0, 0, 0},
{0, 1, 0, 1, 1, 0, 0},
{0, 1, 0, 1, 0, 1, 0},
{0, 1, 0, 1, 0, 0, 1},
{0, 1, 0, 0, 2, 0, 0},
{0, 1, 0, 0, 1, 1, 0},
{0, 1, 0, 0, 1, 0, 1},
{0, 1, 0, 0, 0, 2, 0},
{0, 1, 0, 0, 0, 1, 1},
{0, 1, 0, 0, 0, 0, 2},
{0, 0, 3, 0, 0, 0, 0},
{0, 0, 2, 1, 0, 0, 0},
{0, 0, 2, 0, 1, 0, 0},
{0, 0, 2, 0, 0, 1, 0},
{0, 0, 2, 0, 0, 0, 1},
{0, 0, 1, 2, 0, 0, 0},
{0, 0, 1, 1, 1, 0, 0},
{0, 0, 1, 1, 0, 1, 0},
{0, 0, 1, 1, 0, 0, 1},
{0, 0, 1, 0, 2, 0, 0},
{0, 0, 1, 0, 1, 1, 0},
{0, 0, 1, 0, 1, 0, 1},
{0, 0, 1, 0, 0, 2, 0},
{0, 0, 1, 0, 0, 1, 1},
{0, 0, 1, 0, 0, 0, 2},
{0, 0, 0, 3, 0, 0, 0},
{0, 0, 0, 2, 1, 0, 0},
{0, 0, 0, 2, 0, 1, 0},
{0, 0, 0, 2, 0, 0, 1},
{0, 0, 0, 1, 2, 0, 0},
{0, 0, 0, 1, 1, 1, 0},
{0, 0, 0, 1, 1, 0, 1},
{0, 0, 0, 1, 0, 2, 0},
{0, 0, 0, 1, 0, 1, 1},
{0, 0, 0, 1, 0, 0, 2},
{0, 0, 0, 0, 3, 0, 0},
{0, 0, 0, 0, 2, 1, 0},
{0, 0, 0, 0, 2, 0, 1},
{0, 0, 0, 0, 1, 2, 0},
{0, 0, 0, 0, 1, 1, 1},
{0, 0, 0, 0, 1, 0, 2},
{0, 0, 0, 0, 0, 3, 0},
{0, 0, 0, 0, 0, 2, 1},
{0, 0, 0, 0, 0, 1, 2},
{0, 0, 0, 0, 0, 0, 3},
{4, 0, 0, 0, 0, 0, 0},
{3, 1, 0, 0, 0, 0, 0},
{3, 0, 1, 0, 0, 0, 0},
{3, 0, 0, 1, 0, 0, 0},
{3, 0, 0, 0, 1, 0, 0},
{3, 0, 0, 0, 0, 1, 0},
{3, 0, 0, 0, 0, 0, 1},
{2, 2, 0, 0, 0, 0, 0},
{2, 1, 1, 0, 0, 0, 0},
{2, 1, 0, 1, 0, 0, 0},
{2, 1, 0, 0, 1, 0, 0},
{2, 1, 0, 0, 0, 1, 0},
{2, 1, 0, 0, 0, 0, 1},
{2, 0, 2, 0, 0, 0, 0},
{2, 0, 1, 1, 0, 0, 0},
{2, 0, 1, 0, 1, 0, 0},
{2, 0, 1, 0, 0, 1, 0},
{2, 0, 1, 0, 0, 0, 1},
{2, 0, 0, 2, 0, 0, 0},
{2, 0, 0, 1, 1, 0, 0},
{2, 0, 0, 1, 0, 1, 0},
{2, 0, 0, 1, 0, 0, 1},
{2, 0, 0, 0, 2, 0, 0},
{2, 0, 0, 0, 1, 1, 0},
{2, 0, 0, 0, 1, 0, 1},
{2, 0, 0, 0, 0, 2, 0},
{2, 0, 0, 0, 0, 1, 1},
{2, 0, 0, 0, 0, 0, 2},
{1, 3, 0, 0, 0, 0, 0},
{1, 2, 1, 0, 0, 0, 0},
{1, 2, 0, 1, 0, 0, 0},
{1, 2, 0, 0, 1, 0, 0},
{1, 2, 0, 0, 0, 1, 0},
{1, 2, 0, 0, 0, 0, 1},
{1, 1, 2, 0, 0, 0, 0},
{1, 1, 1, 1, 0, 0, 0},
{1, 1, 1, 0, 1, 0, 0},
{1, 1, 1, 0, 0, 1, 0},
{1, 1, 1, 0, 0, 0, 1},
{1, 1, 0, 2, 0, 0, 0},
{1, 1, 0, 1, 1, 0, 0},
{1, 1, 0, 1, 0, 1, 0},
{1, 1, 0, 1, 0, 0, 1},
{1, 1, 0, 0, 2, 0, 0},
{1, 1, 0, 0, 1, 1, 0},
{1, 1, 0, 0, 1, 0, 1},
{1, 1, 0, 0, 0, 2, 0},
{1, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 2},
{1, 0, 3, 0, 0, 0, 0},
{1, 0, 2, 1, 0, 0, 0},
{1, 0, 2, 0, 1, 0, 0},
{1, 0, 2, 0, 0, 1, 0},
{1, 0, 2, 0, 0, 0, 1},
{1, 0, 1, 2, 0, 0, 0},
{1, 0, 1, 1, 1, 0, 0},
{1, 0, 1, 1, 0, 1, 0},
{1, 0, 1, 1, 0, 0, 1},
{1, 0, 1, 0, 2, 0, 0},
{1, 0, 1, 0, 1, 1, 0},
{1, 0, 1, 0, 1, 0, 1},
{1, 0, 1, 0, 0, 2, 0},
{1, 0, 1, 0, 0, 1, 1},
{1, 0, 1, 0, 0, 0, 2},
{1, 0, 0, 3, 0, 0, 0},
{1, 0, 0, 2, 1, 0, 0},
{1, 0, 0, 2, 0, 1, 0},
{1, 0, 0, 2, 0, 0, 1},
{1, 0, 0, 1, 2, 0, 0},
{1, 0, 0, 1, 1, 1, 0},
{1, 0, 0, 1, 1, 0, 1},
{1, 0, 0, 1, 0, 2, 0},
{1, 0, 0, 1, 0, 1, 1},
{1, 0, 0, 1, 0, 0, 2},
{1, 0, 0, 0, 3, 0, 0},
{1, 0, 0, 0, 2, 1, 0},
{1, 0, 0, 0, 2, 0, 1},
{1, 0, 0, 0, 1, 2, 0},
{1, 0, 0, 0, 1, 1, 1},
{1, 0, 0, 0, 1, 0, 2},
{1, 0, 0, 0, 0, 3, 0},
{1, 0, 0, 0, 0, 2, 1},
{1, 0, 0, 0, 0, 1, 2},
{1, 0, 0, 0, 0, 0, 3},
{0, 4, 0, 0, 0, 0, 0},
{0, 3, 1, 0, 0, 0, 0},
{0, 3, 0, 1, 0, 0, 0},
{0, 3, 0, 0, 1, 0, 0},
{0, 3, 0, 0, 0, 1, 0},
{0, 3, 0, 0, 0, 0, 1},
{0, 2, 2, 0, 0, 0, 0},
{0, 2, 1, 1, 0, 0, 0},
{0, 2, 1, 0, 1, 0, 0},
{0, 2, 1, 0, 0, 1, 0},
{0, 2, 1, 0, 0, 0, 1},
{0, 2, 0, 2, 0, 0, 0},
{0, 2, 0, 1, 1, 0, 0},
{0, 2, 0, 1, 0, 1, 0},
{0, 2, 0, 1, 0, 0, 1},
{0, 2, 0, 0, 2, 0, 0},
{0, 2, 0, 0, 1, 1, 0},
{0, 2, 0, 0, 1, 0, 1},
{0, 2, 0, 0, 0, 2, 0},
{0, 2, 0, 0, 0, 1, 1},
{0, 2, 0, 0, 0, 0, 2},
{0, 1, 3, 0, 0, 0, 0},
{0, 1, 2, 1, 0, 0, 0},
{0, 1, 2, 0, 1, 0, 0},
{0, 1, 2, 0, 0, 1, 0},
{0, 1, 2, 0, 0, 0, 1},
{0, 1, 1, 2, 0, 0, 0},
{0, 1, 1, 1, 1, 0, 0},
{0, 1, 1, 1, 0, 1, 0},
{0, 1, 1, 1, 0, 0, 1},
{0, 1, 1, 0, 2, 0, 0},
{0, 1, 1, 0, 1, 1, 0},
{0, 1, 1, 0, 1, 0, 1},
{0, 1, 1, 0, 0, 2, 0},
{0, 1, 1, 0, 0, 1, 1},
{0, 1, 1, 0, 0, 0, 2},
{0, 1, 0, 3, 0, 0, 0},
{0, 1, 0, 2, 1, 0, 0},
{0, 1, 0, 2, 0, 1, 0},
{0, 1, 0, 2, 0, 0, 1},
{0, 1, 0, 1, 2, 0, 0},
{0, 1, 0, 1, 1, 1, 0},
{0, 1, 0, 1, 1, 0, 1},
{0, 1, 0, 1, 0, 2, 0},
{0, 1, 0, 1, 0, 1, 1},
{0, 1, 0, 1, 0, 0, 2},
{0, 1, 0, 0, 3, 0, 0},
{0, 1, 0, 0, 2, 1, 0},
{0, 1, 0, 0, 2, 0, 1},
{0, 1, 0, 0, 1, 2, 0},
{0, 1, 0, 0, 1, 1, 1},
{0, 1, 0, 0, 1, 0, 2},
{0, 1, 0, 0, 0, 3, 0},
{0, 1, 0, 0, 0, 2, 1},
{0, 1, 0, 0, 0, 1, 2},
{0, 1, 0, 0, 0, 0, 3},
{0, 0, 4, 0, 0, 0, 0},
{0, 0, 3, 1, 0, 0, 0},
{0, 0, 3, 0, 1, 0, 0},
{0, 0, 3, 0, 0, 1, 0},
{0, 0, 3, 0, 0, 0, 1},
{0, 0, 2, 2, 0, 0, 0},
{0, 0, 2, 1, 1, 0, 0},
{0, 0, 2, 1, 0, 1, 0},
{0, 0, 2, 1, 0, 0, 1},
{0, 0, 2, 0, 2, 0, 0},
{0, 0, 2, 0, 1, 1, 0},
{0, 0, 2, 0, 1, 0, 1},
{0, 0, 2, 0, 0, 2, 0},
{0, 0, 2, 0, 0, 1, 1},
{0, 0, 2, 0, 0, 0, 2},
{0, 0, 1, 3, 0, 0, 0},
{0, 0, 1, 2, 1, 0, 0},
{0, 0, 1, 2, 0, 1, 0},
{0, 0, 1, 2, 0, 0, 1},
{0, 0, 1, 1, 2, 0, 0},
{0, 0, 1, 1, 1, 1, 0},
{0, 0, 1, 1, 1, 0, 1},
{0, 0, 1, 1, 0, 2, 0},
{0, 0, 1, 1, 0, 1, 1},
{0, 0, 1, 1, 0, 0, 2},
{0, 0, 1, 0, 3, 0, 0},
{0, 0, 1, 0, 2, 1, 0},
{0, 0, 1, 0, 2, 0, 1},
{0, 0, 1, 0, 1, 2, 0},
{0, 0, 1, 0, 1, 1, 1},
{0, 0, 1, 0, 1, 0, 2},
{0, 0, 1, 0, 0, 3, 0},
{0, 0, 1, 0, 0, 2, 1},
{0, 0, 1, 0, 0, 1, 2},
{0, 0, 1, 0, 0, 0, 3},
{0, 0, 0, 4, 0, 0, 0},
{0, 0, 0, 3, 1, 0, 0},
{0, 0, 0, 3, 0, 1, 0},
{0, 0, 0, 3, 0, 0, 1},
{0, 0, 0, 2, 2, 0, 0},
{0, 0, 0, 2, 1, 1, 0},
{0, 0, 0, 2, 1, 0, 1},
{0, 0, 0, 2, 0, 2, 0},
{0, 0, 0, 2, 0, 1, 1},
{0, 0, 0, 2, 0, 0, 2},
{0, 0, 0, 1, 3, 0, 0},
{0, 0, 0, 1, 2, 1, 0},
{0, 0, 0, 1, 2, 0, 1},
{0, 0, 0, 1, 1, 2, 0},
{0, 0, 0, 1, 1, 1, 1},
{0, 0, 0, 1, 1, 0, 2},
{0, 0, 0, 1, 0, 3, 0},
{0, 0, 0, 1, 0, 2, 1},
{0, 0, 0, 1, 0, 1, 2},
{0, 0, 0, 1, 0, 0, 3},
{0, 0, 0, 0, 4, 0, 0},
{0, 0, 0, 0, 3, 1, 0},
{0, 0, 0, 0, 3, 0, 1},
{0, 0, 0, 0, 2, 2, 0},
{0, 0, 0, 0, 2, 1, 1},
{0, 0, 0, 0, 2, 0, 2},
{0, 0, 0, 0, 1, 3, 0},
{0, 0, 0, 0, 1, 2, 1},
{0, 0, 0, 0, 1, 1, 2},
{0, 0, 0, 0, 1, 0, 3},
{0, 0, 0, 0, 0, 4, 0},
{0, 0, 0, 0, 0, 3, 1},
{0, 0, 0, 0, 0, 2, 2},
{0, 0, 0, 0, 0, 1, 3},
{0, 0, 0, 0, 0, 0, 4}
};
static const double COEF[330][3] = {
{8.70954844857314666e-12, 1.27926950848359881e-09, -2.06865474316332923e-09},
{1.05783308354771544e+00, -8.02119209663359686e-03, -7.88705651445470723e-02},
{1.35905954452774837e-02, 8.71267975138422468e-01, 1.04898760410704936e-01},
{-4.16452026099768252e-02, 1.75465381596434100e-02, 1.00224594702931546e+00},
{4.50321316661211821e-02, -7.11409155427628892e-02, 3.91232300778902690e-03},
{1.76675507851922452e-02, -1.32709276116036640e-01, 6.36935270589509828e-02},
{-5.23434830565911030e-02, 3.77681739012521722e-02, -2.08691145087504179e-02},
{-2.33722556520224792e-03, -1.57542611462692145e-03, -3.05158628452478807e-03},
{-8.87678609044812990e-04, 3.83194388837734693e-04, 1.37779212442523083e-03},
{-2.11519042076831979e-03, 5.82337362515735358e-04, 2.24055108941204821e-04},
{4.61545125563611917e-04, 7.72869451707915893e-04, -1.10800630143346882e-03},
{1.05937484157345879e-03, -3.14448681732842211e-04, -1.75129182446198098e-03},
{1.49045689016363055e-03, -2.09220860101674106e-04, 5.93100338908187697e-04},
{-3.51246656293852696e-04, -8.20743017485394289e-04, 5.71854064480802862e-04},
{-9.18204643629581319e-01, -2.27788122702773155e-01, 6.39980793022790623e-02},
{9.24243491377523679e-05, 7.32841332381495400e-04, -1.55219718415109450e-03},
{7.13695056804217989e-04, -8.46467621879685712e-05, 6.50202947442505750e-04},
{1.66640864747485983e-03, -1.24492362771216523e-04, 2.68236502346156410e-04},
{-7.20253644860527516e-04, 7.81434220384157334e-04, 1.12661089007361367e-03},
{-6.83033334365238206e-05, 7.27742627159490762e-04, -1.78048843835204584e-03},
{-3.13431571993316588e-02, -8.57604034845650287e-01, -2.57225920656276863e-01},
{-6.47867200595898341e-05, -1.16688982572457655e-03, 1.14174511750260031e-03},
{-5.00713925613324338e-04, -6.87598082111323477e-04, 6.20598069880440176e-04},
{-8.56716727659588957e-05, 9.74478786593559361e-04, -1.65892838405139512e-03},
{6.53468478750158263e-04, 7.51662000672516676e-04, -6.73196326298856570e-04},
{-4.42539011000103941e-02, -2.01965359697350230e-02, -9.94663493761314355e-01},
{-7.39107395392403087e-04, 5.28870828612476996e-04, 1.00947183860234540e-03},
{-2.06577300933763214e-03, 9.60215813758718011e-04, -3.27993888180819421e-04},
{3.47783280638377555e-04, 8.41824316850705743e-04, -8.87458944147930993e-04},
{1.20960551709587905e+00, -7.07660818059813873e-02, -8.56332806008946491e-03},
{2.11116509318935269e-04, 7.68490846994171776e-04, -1.63228995491542417e-03},
{6.47698075356516103e-04, -4.20589129268072884e-04, 1.18354001300614896e-03},
{-2.78795945253848716e-02, 1.22199201000304547e+00, -2.07383075858847743e-01},
{-5.32457386680677347e-05, -9.58027320315790677e-04, 9.89667309649038679e-04},
{-9.03932426306289782e-02, -4.00969232187064692e-02, 1.26285611182120072e+00},
{-2.19453630740322871e-03, -1.21893190049422620e-03, -1.92293368093085417e-03},
{1.72950845415964505e-06, -8.93952511560151819e-09, -6.14874900641340649e-06},
{8.02644554976326974e-06, -6.42543741723487294e-06, -6.07103419227907060e-06},
{3.20307552755319525e-06, -4.83533743093466500e-06, 9.13563764113473065e-07},
{-2.18105804067510178e-06, 6.19595552598436322e-07, 5.21392855381760945e-06},
{-2.43310123604345563e-06, 2.17201813434465818e-06, 1.94098874242362718e-07},
{-1.56293672065252465e-06, 3.95256011818110372e-06, 1.68792962079201969e-06},
{-1.37567295252127852e-03, 3.59746071987262106e-04, 7.38927139000157259e-05},
{4.27822004137219658e-06, -8.80187479967658548e-07, 2.29453131891411977e-06},
{7.68758937964332534e-06, 2.40909410585557829e-07, 4.69351234070854509e-06},
{-2.87166709944317033e-06, 7.60223902901142716e-07, 4.57864913314467992e-06},
{-4.01295140267654560e-06, 2.65929275888376483e-06, -2.36575067819565221e-06},
{2.32693030513910805e-07, 2.28814396769890308e-06, 1.83526107699893970e-07},
{-2.18213927011287265e-03, 1.65013083920367864e-03, 2.31992998847323087e-04},
{-7.70829764693697905e-06, 4.23888841240673345e-07, 7.30018322002944087e-06},
{-1.23111329452911533e-06, 1.50076529718910084e-06, -1.91139744928209288e-06},
{-1.68872756433485760e-06, 1.03254236824697979e-06, -1.72081108163607555e-06},
{1.64276928199709460e-06, -4.96350219553231067e-07, -1.46349385185670297e-06},
{1.12731767057843682e-03, 5.03104281148445223e-04, 1.36398977654308994e-03},
{-1.05449609518089293e-06, -4.06952115309007489e-07, 3.53062441379482783e-06},
{-1.98745923822574166e-06, 4.98021943693208180e-07, 3.92645061370218429e-06},
{-1.55569377977005097e-07, -4.00262856484093037e-07, -2.49609122397048688e-06},
{2.18005022830924673e-03, -4.10275057064835439e-05, -2.59776311836759947e-04},
{5.41337439827552225e-07, -1.88603932528607146e-06, -2.06428606152470051e-06},
{-6.03243799807140491e-06, -3.75067864464502022e-06, -3.05702776851046742e-06},
{2.30038011634901016e-03, -1.32581161861259635e-03, -1.07680096899188406e-03},
{4.46773877910556887e-06, 1.85008408528524772e-08, -2.72851357570281713e-06},
{-1.49177636513049289e-03, -1.91426739654176659e-04, -1.71206384332753194e-03},
{2.31661325589237743e-02, 2.26540538563063554e-01, 5.42330337046266139e-02},
{-1.40563059963100256e-06, -4.50551806294901061e-06, 8.87542894832671347e-06},
{-1.66780916452391459e-06, 4.12065434881171526e-06, -3.55865035776836702e-06},
{2.71536622051954390e-07, -3.08564858926584692e-06, -1.52164363662402047e-06},
{2.66659632027280158e-06, -1.19436686895073481e-06, -3.25738306279285683e-06},
{-1.43666282346327501e-06, -2.51923473623639690e-06, 5.21205120344175876e-06},
{2.82954522469612199e-04, -1.59147454710008968e-03, 1.27685773978167098e-03},
{-3.99471240294241303e-06, 9.97323772325767188e-08, -5.28196823261495307e-06},
{-6.39858432699424995e-06, -4.59897864440506933e-06, -2.39736149785715891e-06},
{2.89457420106498109e-06, -3.10427512149489757e-06, 9.75553221437691631e-07},
{-8.96518259720091581e-07, -5.53996694461914366e-06, 1.03733964032237669e-05},
{8.82130497168875905e-04, -2.33618402105562365e-03, 1.35100410641244379e-03},
{-2.14088521029685841e-06, 2.59005410360388117e-06, -9.78713171504927426e-08},
{-4.50668337071552516e-06, 3.58808570076458002e-06, -1.56159349007541082e-06},
{-1.52345101244247272e-06, 2.21066768791959578e-06, -2.19555898547246775e-06},
{2.07334042074768356e-03, -1.56333498489329517e-03, -5.53762940364141767e-04},
{2.22151748134440108e-06, -4.74729938900429749e-07, -3.46744150304684889e-06},
{2.95389009221172505e-06, -2.96312023445686329e-06, -9.00385068308695580e-07},
{-6.47780848348620771e-04, 2.38772263398574292e-03, -8.93908589731968019e-04},
{9.69501567645025819e-07, 2.41432205872957328e-06, 5.56908291093893837e-07},
{-6.33392066185247586e-04, 2.38613844267241120e-03, -1.05383725637261472e-03},
{6.76250135616376785e-02, -5.57799579151454852e-02, 1.83393652374666566e-01},
{3.53986894266120067e-06, 5.92996717102502093e-06, -7.32378536156402804e-06},
{5.69667193362453916e-06, 1.20219201908705218e-06, -4.56663805956276925e-06},
{7.11494218295222192e-07, 2.93069858359131137e-06, 1.23210839732268429e-07},
{-3.41917893741799928e-06, -1.47435291776966751e-06, 1.07397354370819542e-06},
{7.30931882734254710e-04, 1.15433149094644884e-03, -2.40026982569019722e-03},
{-1.22780859907432871e-06, 2.29287908084027789e-06, 1.84270754640877832e-06},
{7.71579140080615178e-07, 2.92378122615943208e-06, -1.91800935486416413e-07},
{-3.76107279903559188e-07, -1.83159743461489867e-06, 8.17089655984204466e-07},
{-1.10830882430058061e-03, -5.10908079549339251e-04, -1.77835176235151705e-03},
{-1.26839781743699406e-06, -2.86942252006448415e-06, 4.47464983859263005e-06},
{-1.44518716284694482e-06, -7.03360635528004451e-06, 1.04898109513258675e-05},
{-4.98687888007460470e-04, 1.86990180752567262e-03, -1.24341018156770089e-03},
{-2.90479801332704790e-06, -9.24272269110706229e-07, 7.56354222045119151e-07},
{-1.16451534008294149e-03, -2.34216801827852273e-03, 4.91479264672447288e-03},
{-7.70970926241258958e-02, 9.35855573900774423e-02, 1.50623807158846906e-01},
{1.14039905307547484e-06, -1.80664235182388840e-07, -5.15527441317074897e-06},
{7.50559587697416375e-06, -6.23982034686780714e-06, -5.01245198064126721e-06},
{2.37840954889385892e-06, -4.15663063190341991e-06, 1.93118829429697603e-06},
{-1.54903048110950777e-03, 2.65832194444263125e-04, 5.34401520444913940e-04},
{4.00040634507183718e-06, -2.43965474694277443e-06, 2.88683251413283937e-06},
{7.72301916160400559e-06, -9.54300275625495457e-07, 5.50777546561020959e-06},
{-2.28103126593574368e-03, 1.02658341009706066e-03, 1.22010567464172614e-03},
{-6.32818026002207601e-06, 9.83088209200334157e-07, 5.24316808343458507e-06},
{1.37175660779395581e-03, 4.01188715721313943e-04, 7.59370199245276625e-04},
{-3.33184694847917573e-01, 7.82846225823195241e-02, -9.94270054263078074e-02},
{-1.70108770909324636e-06, -5.10749831734438279e-06, 9.80267482880020635e-06},
{-1.79301365419055891e-06, 4.44839673308561508e-06, -3.83837422072638712e-06},
{1.71911692904483371e-04, -1.56077480341044431e-03, 1.30725115579017584e-03},
{-3.55763938679129477e-06, 1.20558966207589408e-06, -5.94340114624253291e-06},
{1.02325453537648178e-03, -1.52640960762801372e-03, 3.10973117856692537e-04},
{3.81842873295820109e-03, -3.02114884453467680e-01, 2.78264587142456665e-01},
{3.46123498726202961e-06, 5.05929187103208375e-06, -6.85764673719752027e-06},
{4.47228353489932293e-04, 9.60672217798415784e-04, -2.19382758010531077e-03},
{2.22711833124298791e-01, -4.14141995162802465e-02, -4.27998216564745015e-01},
{-1.78271151817048783e-03, -9.81039111371464307e-04, -1.37513011841553174e-03},
{3.35305394560947434e-10, -1.26710751613412498e-09, 3.54248685940916630e-09},
{-9.26917423371698135e-09, 6.21190912597491263e-09, 1.86942252233812667e-08},
{-1.56687696151180944e-09, -5.44315731376698864e-09, 1.93822974337010123e-09},
{7.52897716393974292e-10, -3.48923168136394679e-10, -5.94217786087369859e-10},
{2.52116855170569920e-10, -2.48216903975251313e-09, 1.01699001303634518e-09},
{3.72215577457146729e-09, 4.51910314724912610e-10, -6.15361639422218332e-09},
{-2.62088816666700142e-07, 3.23631086683010168e-07, 8.85302852722882894e-07},
{-1.30537319842360944e-08, 1.46808588619151692e-08, 2.67574040702101001e-09},
{-1.23991327621864045e-08, 2.61298349069072344e-08, -4.58919307373337193e-09},
{5.03079244928983371e-09, -6.73783119575777079e-10, -1.13935871848269699e-08},
{9.09065785148488459e-09, -1.04304054004966673e-08, -3.23123813816827976e-09},
{9.55627910137479830e-10, -1.41129563591135820e-08, -1.75594400131373618e-09},
{-1.05549669436946769e-07, 8.47284096194811896e-08, 6.70761880091491625e-07},
{-5.92079330008488114e-10, 6.31702118392141188e-09, -4.51534448719925763e-09},
{-1.04033970327321867e-09, 4.67775485013532943e-09, 2.79348504744758586e-09},
{5.38758108958869997e-09, -9.55380699552144108e-09, 6.16488249338686956e-11},
{1.12057409185073453e-09, -3.00645183748393663e-09, -2.14940637510707688e-09},
{-6.27004681934967278e-07, 8.59159786402940127e-07, 2.73192537668387470e-07},
{7.36784189214745311e-10, -8.12761968838060511e-10, -2.43226564583531868e-09},
{1.25546123497244366e-09, -6.98609614602219153e-10, -5.29894812750786315e-09},
{-8.88351475714088679e-10, 1.37132565025677167e-09, 1.92497813869541012e-09},
{6.10992637326349119e-07, -6.13496367368217277e-07, -2.19901889726877020e-06},
{-8.59090437677068053e-11, 2.72772732179404898e-09, 1.54554039011323141e-09},
{-4.58798915525804318e-10, 4.54384851966693759e-09, 3.63189350816028877e-09},
{9.93115786933340683e-08, 1.63700862245048928e-07, -1.71397937400244449e-07},
{-1.62985361318312982e-09, -3.10762126448649312e-09, 1.76193495557419588e-09},
{6.27207737564569601e-07, -1.49343052365004934e-06, 8.16168870109573730e-08},
{1.42518738380244172e-03, -3.47531891583186285e-04, -2.98661838800559913e-04},
{8.98157254125564464e-09, -8.24242643235328920e-09, -5.34769730234363472e-09},
{-2.17776999489327494e-08, -4.47141107473569832e-09, -1.10218517090920898e-08},
{3.19614509858290319e-09, -3.32861183754973311e-09, 9.92016746526047655e-11},
{-2.91660393059167689e-09, 5.59829099744391101e-09, 1.70080685646389895e-09},
{1.22479524179014421e-09, 9.20737683318684219e-09, -1.10618757209746121e-10},
{7.70594587548882257e-09, -1.33267446898667659e-06, 4.52812675308736368e-07},
{9.46080642993951670e-09, -1.95483249032513129e-08, -1.23592694620255905e-08},
{-2.02330094345448686e-09, 1.18198534293512125e-10, 2.34746776184291406e-09},
{4.00839940406516604e-09, -4.80716730311137042e-09, 5.25802457129742606e-09},
{-2.53115202408782380e-09, 2.05563177591017165e-10, 5.46003270374129102e-09},
{3.24841319972028232e-08, -1.24284705839720552e-06, 4.97326549863015555e-07},
{1.37729661009444726e-09, -1.67903983772088594e-09, -5.62083748989472554e-09},
{-3.53256937590806785e-10, 4.49320892992322030e-09, -4.02300486673778934e-09},
{2.48976475547557641e-09, -6.97256366533061112e-09, 1.43185084622299286e-09},
{-4.38617299338556199e-09, 9.45081248826811111e-08, -2.91197460585562728e-07},
{3.24429103026879773e-09, -1.71647943601749287e-09, 2.71076100455402980e-09},
{3.86933235105302309e-09, -2.82628156988984358e-09, 8.24455756442965537e-09},
{-7.46614068323353530e-07, 1.27696340529665289e-06, 6.88413034833322557e-07},
{-5.78118683480788320e-09, 1.34319005917760137e-09, -1.15898873831454807e-09},
{4.42686972671260670e-07, 6.41810588767341775e-07, -1.16058405342719939e-08},
{2.24399192788231686e-03, -1.35129336477888174e-03, -7.39944244498236844e-04},
{7.47869199901884940e-09, -2.68762612165573955e-09, -7.41584788022109365e-09},
{1.80867308283150230e-09, -2.21500551234043996e-09, 1.86995768869380186e-09},
{-5.05514829302056157e-09, 4.74048706539109688e-09, 2.52998993977016085e-09},
{1.32441967115592973e-09, 5.70339246663831290e-09, 7.13448300437846683e-10},
{1.19767475292940212e-06, 6.72445227582811568e-07, -1.97500319605841551e-06},
{-1.70612399208458498e-09, 1.07145120553653328e-09, 1.73225882249550267e-09},
{1.15369127445807962e-09, -5.80362996549510513e-09, 9.33515653667171819e-10},
{3.38692740520230018e-09, 3.72531013675958533e-09, -3.18062756687886861e-09},
{1.14787653780236421e-06, -1.84917201319622368e-06, -2.44834286920736499e-07},
{1.45558928799083276e-09, 1.12720083267348059e-09, 9.00940544390493869e-10},
{2.09654001104286891e-09, 4.92913422578400429e-09, 3.04938074791039071e-10},
{3.54033623213741155e-07, 1.07259516691213860e-06, -6.03027205987524684e-07},
{-2.72038239157446071e-09, -1.60070143945256760e-09, 6.03853855807301443e-10},
{-2.03235662485238069e-06, -1.03151962834260348e-06, 1.99637918628457062e-06},
{-1.26261175077493210e-03, -4.98503988506484859e-04, -1.03875859619143593e-03},
{6.43182729298530376e-10, 8.01776645076301975e-10, -1.83589794755523172e-09},
{4.01805119037978997e-09, -5.63673552278487477e-10, -1.09102650663883693e-08},
{-1.48648961195707585e-09, 5.01067861508053269e-09, 2.99132781045319263e-09},
{-8.91404754824534629e-07, 7.49163968581634775e-07, 2.12542215183124383e-06},
{2.38642574451608525e-09, -3.47605810802065207e-09, 3.86935566920598717e-10},
{-2.80031986488182838e-09, -4.25160427697246490e-11, 2.24182921879090280e-09},
{-1.26991357818351247e-07, -1.45348284568834647e-07, 5.68792533226815389e-07},
{1.39227229745131353e-09, -1.84849578699353145e-09, 2.24967258190267305e-09},
{-1.15462500328497586e-06, 1.84347590761761086e-06, 3.64918716654494962e-07},
{-2.09357112083411985e-03, 1.60820400301404873e-05, 2.27418117008655948e-04},
{-1.04484803378768198e-08, 4.86043558178828050e-09, 2.00996588123336650e-09},
{1.44040971927772432e-08, 1.42223015309195233e-09, 1.99778974613318283e-09},
{-1.62414574166394599e-07, -1.31976785339561840e-06, 4.43918084507000099e-07},
{3.73061943836905385e-09, 1.00036822436866402e-08, -1.05450977117005351e-09},
{-2.06551932971539565e-07, -9.72167971235462190e-07, 4.28861904300768815e-07},
{-2.16051814014425313e-03, 1.48780488507118812e-03, 7.79940397419977911e-04},
{-4.80544204428667854e-09, -1.09870773590259319e-09, 6.58876991984844174e-09},
{1.31575045692056136e-06, 4.32430764481131318e-07, -1.55255090541518703e-06},
{1.28823975640215602e-03, 4.04521283440268135e-04, 1.76186984141882253e-03},
{-1.09767251093991436e-01, -4.94112205838347640e-02, -5.43102978164306804e-02},
{7.93691223854864347e-10, 1.54639511196208446e-08, -1.71518303448969789e-08},
{2.56523843833456056e-09, -2.31047392329486456e-09, -4.29758133398648601e-09},
{-9.87725901069325118e-09, 4.28127375218245732e-09, 2.02888056355376989e-09},
{3.21762172461603768e-10, -5.82937505211322815e-09, 3.88293127512318037e-09},
{1.63250610252241302e-09, -7.02161705168347083e-09, 3.46592492032893329e-09},
{-1.44272117683086343e-07, -4.40408510988914148e-07, 5.92746408872857344e-07},
{2.71961467235293242e-09, -1.47466668633244868e-08, 2.89637452632884873e-08},
{1.47637712476396399e-08, 1.16406781783262581e-09, 2.04904540557215853e-09},
{-5.53709807865621073e-09, 7.05512286092169205e-09, 1.56159114805820565e-09},
{5.29268649740455288e-09, 2.10616986628942016e-08, -3.03219004488264332e-08},
{1.79978890693655025e-07, 7.95085399132693105e-07, -4.78366567607801940e-07},
{-4.03847393894152251e-10, 2.90357085597214848e-09, 1.12992165623992946e-09},
{2.99031871486832301e-09, -1.37951879780606745e-09, 2.41048263988075107e-09},
{1.26882357398550027e-09, 1.30631467101793852e-09, 7.99574240151201820e-10},
{-1.41169562567489137e-08, 1.27148955713198356e-06, -2.89386439707162157e-07},
{-2.68794415198003733e-09, 8.73673404455654889e-10, 2.89557382238125882e-09},
{-4.90264437380538709e-09, 1.89207244316591527e-09, 2.25393465003165261e-09},
{-3.58274654665979853e-08, 2.91386646529383231e-07, -4.98477764412919022e-08},
{1.65722165851311942e-09, -1.11673743863338615e-09, -4.14131162695952071e-09},
{-1.47751280626939874e-07, -2.41471865000848773e-07, -8.53552350049691100e-07},
{-2.24352957583577790e-04, 1.60900273524284708e-03, -1.32260753549593617e-03},
{2.05497643901431104e-09, 1.38702982710459111e-08, -3.09887516689033582e-09},
{3.39770491949997755e-09, 9.41613393506957053e-09, -7.09844738544518350e-10},
{7.86209687630989862e-10, 1.93556837224662104e-10, -6.58630930350234678e-09},
{-6.86841181152253455e-10, -5.57194149153339424e-09, 1.41214109156129197e-09},
{2.59516074158083754e-07, 1.30703181255419770e-06, -4.02454784192984860e-07},
{-5.79425202262839889e-10, 4.05071760856134944e-09, 3.02384985106929349e-09},
{4.00677924866643664e-09, -2.25614611715219127e-09, 7.52819043214891792e-09},
{2.34003759425061020e-09, 5.27462258592681366e-09, -2.05723854618256041e-10},
{2.29340174767722615e-07, 1.05507868574435809e-06, -4.45904844964539748e-07},
{-3.91634245866523401e-09, 1.07849931763048801e-09, 1.85542686770290288e-09},
{-6.62166513287765213e-09, 3.86355018811013196e-09, -1.87861701195224384e-09},
{1.32112240848469842e-07, 4.39339645861430705e-08, -1.59384598983486336e-06},
{2.02488462108796341e-09, -1.48427112267590644e-09, -4.32055485832805175e-09},
{-4.27701540045566375e-07, -1.46229443391283215e-06, -2.38186369433401879e-07},
{-9.86744509368740232e-04, 1.91104095070606826e-03, -8.17774843405986713e-04},
{2.06891823117949514e-10, -2.64060942556376688e-09, 1.86419366055012858e-09},
{8.33785634979378187e-09, -1.00697171434571686e-08, -2.84106664583116952e-09},
{5.07057938692323518e-09, -9.56246298811080919e-09, -6.33399999117045809e-11},
{-6.78808357162941078e-08, -2.21612941845184680e-07, 9.42031624998063144e-08},
{-3.04300065007145903e-09, 5.64120231083542478e-09, 1.65718606892628628e-09},
{3.76240642807612602e-09, -4.58941407446844529e-09, 5.06162500801821125e-09},
{7.25149885354159363e-07, -1.18149759075966698e-06, -6.82406347277120240e-07},
{-4.84358128605144600e-09, 4.56893046833772853e-09, 2.67044331092591847e-09},
{-2.54939737986958903e-07, -1.06106228658746360e-06, 5.04013386790069795e-07},
{-2.17097468872509735e-03, 1.41624400187313607e-03, 8.11305605779899562e-04},
{2.24635331169675823e-10, -6.02144184513875302e-09, 4.15827878380570226e-09},
{-4.55408258326350790e-09, 6.20319154376325343e-09, 2.08760821823750220e-09},
{2.10871853867367065e-07, -4.29346688506603014e-07, 1.15683623843482186e-07},
{1.00732072683129559e-09, 3.88267751283422058e-11, -6.73798626615873530e-09},
{5.34506627847264326e-09, -8.01262819982717645e-08, 1.60888846226225901e-06},
{5.83419066552946048e-04, -2.36474094848551555e-03, 8.79373865688287898e-04},
{-4.85158746510450101e-10, -6.78789624508624456e-09, 4.95385649168511577e-09},
{3.47485142271342085e-07, 5.60944792101468470e-07, -4.35887910682497548e-07},
{5.75824910919892421e-04, -2.18618554413632388e-03, 1.22736498224538170e-03},
{-2.51838883195707221e-02, -8.23487774284355212e-02, 3.33658831723806573e-02},
{-8.70167529698484543e-09, -1.37080219501928280e-08, 1.80728228771354082e-08},
{-4.67111571644807100e-09, -2.72041008123058425e-09, 7.06648883852523113e-09},
{7.26183221906172727e-10, -6.77816339167414128e-09, 4.52883232651690726e-09},
{5.28852302228433047e-09, 6.47161005340457507e-09, -8.67298467766008940e-09},
{-2.25465519365641853e-07, -6.46057585221293529e-07, 3.48151143400587948e-07},
{-1.30051025504229756e-09, -3.25062288891730944e-09, 2.01775679498084060e-09},
{-5.12724809831333062e-09, 9.33902577666956280e-10, -6.96327353416625883e-10},
{-3.10810940873373909e-09, -7.49756534634826721e-10, 6.87357185058523612e-10},
{-1.52109221995821997e-06, -4.22908767925417317e-07, 1.38629667568307413e-06},
{1.42955317028459206e-09, -7.02968461219199980e-10, -3.81617160094549490e-09},
{2.53707400921232562e-09, -1.60727622877665510e-09, -4.18765366827500429e-09},
{-2.14750738948554787e-07, -6.40554276953864132e-07, 3.76128531993924486e-07},
{3.83073214815787821e-09, 4.50296289838947317e-10, 2.29523194894554194e-09},
{4.76340728555735282e-07, 6.83235613037347367e-07, -4.72205395646296822e-07},
{-6.10651996176347607e-04, -1.06790499934057291e-03, 2.29083496655867842e-03},
{3.95497823379997726e-09, 1.38236928154400474e-09, -6.26218820548585242e-09},
{1.11904936705986557e-09, -1.37869946362223494e-08, -9.34049783699042457e-10},
{1.25499246411697740e-09, -2.73635453185150368e-09, -2.91506864740637139e-09},
{-3.59882924006599270e-07, 1.32511373732895413e-06, -1.55110207063907657e-07},
{1.07068498511608823e-09, 8.92087770321126072e-09, 2.62826524433101838e-10},
{-2.69316546841480431e-09, 9.61138280075601870e-10, 5.19946977139973399e-09},
{-5.92563579700916554e-07, -1.05071339539294234e-06, 1.56249964602256375e-07},
{1.32198180180509439e-09, 5.16087961255351502e-09, 8.46339526239248130e-10},
{2.07323220008381881e-06, 1.02309267446332522e-06, -2.07661522726165781e-06},
{1.31402366846389393e-03, 3.78229792813366064e-04, 1.77496793932758741e-03},
{8.59301428624004160e-10, -6.83071707530125138e-09, 3.36249680876754553e-09},
{5.27310424491833629e-09, 2.09999085065692981e-08, -3.10459945807028959e-08},
{-8.88666080375855039e-08, 4.60897593930476024e-07, 7.41576575386676540e-07},
{-4.85540663230921155e-10, -5.58243438975036810e-09, 7.40450811775872353e-10},
{4.03141117225058743e-07, 1.52035531639227450e-06, 9.06206514897367477e-08},
{5.61075629915620496e-04, -2.05847905628765053e-03, 1.12849817492909434e-03},
{5.11216541321246609e-09, 7.26292920250060092e-09, -8.97145741030058730e-09},
{-4.26211688914213127e-07, -7.03366608210270750e-07, 6.27995585866791828e-07},
{1.15309052943982646e-03, 2.34474318844151959e-03, -4.91856748507475423e-03},
{1.01104427799588961e-01, -4.22361682938472982e-02, -1.88750007538552200e-01},
{3.94738332298860684e-10, -7.81372397340440727e-10, 4.06815717224340290e-09},
{-8.61483928638051566e-09, 5.37427180535843263e-09, 1.81738104426676372e-08},
{-8.48011268844706123e-10, -5.33803143354383280e-09, 2.99703953494934172e-10},
{3.89154099408092063e-07, -2.44166311268514957e-07, -8.03240371135063858e-07},
{-1.20249536439409610e-08, 1.48908931921210019e-08, 1.88292573199966284e-09},
{-1.16401289163015065e-08, 2.57866422936903206e-08, -5.27022399332555125e-09},
{1.37065399911928676e-07, 2.16494406102361175e-08, -7.63924557662179482e-07},
{-6.94754161319199870e-10, 6.65038621394664631e-09, -4.31779645371221932e-09},
{4.72542155592614588e-07, -7.58546986886782931e-07, -2.35913417925837088e-07},
{1.46133817312113241e-03, -3.25193103208258009e-04, -3.06625181254991741e-04},
{9.35794082672593210e-09, -7.92923574022275091e-09, -5.41426242728348939e-09},
{-2.15279239157428748e-08, -4.16754339024882903e-09, -1.12896482995505920e-08},
{2.60645369870582400e-10, 1.44616071127263122e-06, -3.63334053799999057e-07},
{9.17105741349288905e-09, -2.02295233654725681e-08, -1.20002956877085509e-08},
{-1.27759226226098477e-07, 1.28193771791124470e-06, -5.83097827522305323e-07},
{2.26880791869919426e-03, -1.34042850080092401e-03, -7.65092051285704835e-04},
{7.03374036792325796e-09, -2.53508958270032281e-09, -7.66132998708535240e-09},
{-9.71978722189015265e-07, -5.57836512454779054e-07, 1.96329328074063003e-06},
{-1.26115140811304343e-03, -4.81792074617704632e-04, -1.06803272537897391e-03},
{1.19419564863885497e-01, 5.07766738901840875e-02, 4.87642090320925953e-02},
{1.14090414893297520e-09, 1.56073433760228752e-08, -1.78054684078429726e-08},
{3.03285130343056153e-09, -1.58615337531031741e-09, -4.94928394101368241e-09},
{2.64483280249840080e-07, 2.97155396291660413e-07, -5.41608085095034164e-07},
{2.68757552324139226e-09, -1.41400907649469332e-08, 2.93255796729452456e-08},
{-2.11094617584561828e-07, -6.56355695552793272e-07, 3.72180321686621518e-07},
{-2.55073452371079590e-04, 1.57943859317488818e-03, -1.29154484940938240e-03},
{1.40049266628139435e-09, 1.40747080656922208e-08, -2.58792021839981956e-09},
{-2.12330362681090179e-07, -1.30522733223815968e-06, 5.84417623253341567e-07},
{-9.33144849909676392e-04, 1.90305575962152547e-03, -8.35564417983726418e-04},
{1.81624805201406961e-02, 6.84911174969819458e-02, -2.28291882522520390e-02},
{-8.25231299961259879e-09, -1.40227519596081152e-08, 1.78809529925716415e-08},
{1.90689491530449118e-07, 7.01057736002264065e-07, -4.26430629252294580e-07},
{-5.85146839837499930e-04, -1.07311215649546045e-03, 2.31986890222730339e-03},
{-1.05962397073886522e-01, 5.51532131360410807e-02, 1.87542648909451215e-01},
{-1.37499370823599516e-03, -8.49619409242363438e-04, -1.18180356709159952e-03}
};
static const double INTERCEPT[3] = {
-1.29208772400146188e+00,
6.62251952866635918e+00,
-1.35908984683965173e-01
};
// END AUTO-GENERATED COEFFICIENTS
inline void compute_poly_features(const double x[7], double out[330]) {
for (int i = 0; i < N_FEATURES; ++i) {
double val = 1.0;
for (int j = 0; j < N_INPUTS; ++j) {
if (POWERS[i][j] != 0) {
double base = x[j];
int exp = POWERS[i][j];
// Fast integer exponentiation (max exp = 4)
double p = 1.0;
for (int e = 0; e < exp; ++e)
p *= base;
val *= p;
}
}
out[i] = val;
}
}
} // namespace detail
struct RGB {
unsigned char r, g, b;
};
/**
* Mix two RGB colors using polynomial pigment mixing.
*
* This performs polynomial pigment-style RGB interpolation.
*
* @param r1,g1,b1 First color (0-255)
* @param r2,g2,b2 Second color (0-255)
* @param t Mixing ratio: 0.0 = all color1, 1.0 = all color2
* @param out_r,out_g,out_b Output color (0-255)
*/
inline void lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t,
unsigned char* out_r, unsigned char* out_g, unsigned char* out_b) {
// Clamp t
if (t <= 0.0f) {
*out_r = r1; *out_g = g1; *out_b = b1;
return;
}
if (t >= 1.0f) {
*out_r = r2; *out_g = g2; *out_b = b2;
return;
}
double x[7] = {
static_cast<double>(r1), static_cast<double>(g1), static_cast<double>(b1),
static_cast<double>(r2), static_cast<double>(g2), static_cast<double>(b2),
static_cast<double>(t)
};
double features[330];
detail::compute_poly_features(x, features);
// Dot product: features @ COEF + INTERCEPT
for (int c = 0; c < 3; ++c) {
double sum = detail::INTERCEPT[c];
for (int i = 0; i < detail::N_FEATURES; ++i) {
sum += features[i] * detail::COEF[i][c];
}
// Clamp to [0, 255] and truncate (matches numpy astype(int) behavior)
int val = static_cast<int>(sum);
if (val < 0) val = 0;
if (val > 255) val = 255;
if (c == 0) *out_r = static_cast<unsigned char>(val);
else if (c == 1) *out_g = static_cast<unsigned char>(val);
else *out_b = static_cast<unsigned char>(val);
}
}
/**
* Convenience overload returning an RGB struct.
*/
inline RGB lerp(unsigned char r1, unsigned char g1, unsigned char b1,
unsigned char r2, unsigned char g2, unsigned char b2,
float t) {
RGB result;
lerp(r1, g1, b1, r2, g2, b2, t, &result.r, &result.g, &result.b);
return result;
}
} // namespace filament_mixer
#endif // FILAMENT_MIXER_H
+12
View File
@@ -373,6 +373,18 @@ set(SLIC3R_GUI_SOURCES
GUI/FilamentGroupPopup.cpp GUI/FilamentGroupPopup.cpp
GUI/PhysicalPrinterDialog.cpp GUI/PhysicalPrinterDialog.cpp
GUI/PhysicalPrinterDialog.hpp 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.cpp
GUI/Plater.hpp GUI/Plater.hpp
GUI/PlateSettingsDialog.cpp GUI/PlateSettingsDialog.cpp
+8 -4
View File
@@ -614,6 +614,9 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
if (shader) { if (shader) {
if (idx == 0) { if (idx == 0) {
int extruder_id = model_volume->extruder_id(); 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 //to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]); ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[extruder_id - 1]);
if (ban_light) { if (ban_light) {
@@ -623,7 +626,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
// shader->set_uniform("uniform_color", new_color); // shader->set_uniform("uniform_color", new_color);
} }
else { else {
if (idx <= extruder_colors.size()) { if (idx <= (int)extruder_colors.size()) {
//to make black not too hard too see //to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]); ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]);
if (ban_light) { 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); std::vector<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
TriangleMesh wipe_tower_shell = make_cube(width, depth, height); TriangleMesh wipe_tower_shell = make_cube(width, depth, height);
for (int extruder_id : plate_extruders) { 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]); colors.push_back(extruder_colors[extruder_id - 1]);
else else
colors.push_back(extruder_colors[0]); 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<int> plate_extruders = ppl.get_plate(plate_idx)->get_extruders(true);
std::vector<Slic3r::ColorRGBA> colors; std::vector<Slic3r::ColorRGBA> colors;
if (!plate_extruders.empty()) { if (!plate_extruders.empty()) {
if (plate_extruders.front() <= extruder_colors.size()) const int front_id = plate_extruders.front();
colors.push_back(extruder_colors[plate_extruders.front() - 1]); if (front_id >= 1 && front_id <= (int)extruder_colors.size())
colors.push_back(extruder_colors[front_id - 1]);
else else
colors.push_back(extruder_colors[0]); colors.push_back(extruder_colors[0]);
} }
+98 -11
View File
@@ -479,20 +479,43 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
} }
} }
// BBS // Rule 1:
static const char* keys[] = { "support_filament", "support_interface_filament"}; // - support slots stay physical-only
for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) { // - feature slots (wall/infill/wipe tower) may use mixed virtual IDs
std::string key = std::string(keys[i]); // 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)); auto* opt = dynamic_cast<ConfigOptionInt*>(config->option(key, false));
if (opt != nullptr) { if (!opt)
if (opt->getInt() > filament_cnt) { 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; DynamicPrintConfig new_conf = *config;
const DynamicPrintConfig *conf_temp = wxGetApp().plater()->config(); new_conf.set_key_value(key, new ConfigOptionInt(0));
int new_value = 0; apply(config, &new_conf);
if (conf_temp != nullptr && conf_temp->has(key)) {
new_value = conf_temp->opt_int(key);
} }
new_conf.set_key_value(key, new ConfigOptionInt(new_value)); }
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); apply(config, &new_conf);
} }
} }
@@ -541,6 +564,40 @@ void ConfigManipulation::update_print_fff_config(DynamicPrintConfig* config, con
apply(config, &new_conf); apply(config, &new_conf);
is_msg_dlg_already_exist = false; 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) 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); 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)); 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*/) void ConfigManipulation::update_print_sla_config(DynamicPrintConfig* config, const bool is_global_config/* = false*/)
+35 -6
View File
@@ -263,6 +263,33 @@ bool BitmapChoiceRenderer::GetValue(wxVariant& value) const
return true; 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) bool BitmapChoiceRenderer::Render(wxRect rect, wxDC* dc, int state)
{ {
const wxBitmap& icon = m_value.GetBitmap(); 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()) if (can_create_editor_ctrl && !can_create_editor_ctrl())
return nullptr; return nullptr;
std::vector<wxBitmap*> icons = get_extruder_color_icons(); std::vector<wxBitmap*> icons = mixed_aware_extruder_icons(false);
if (icons.empty()) if (icons.empty())
return nullptr; return nullptr;
@@ -317,12 +344,14 @@ wxWindow* BitmapChoiceRenderer::CreateEditorCtrl(wxWindow* parent, wxRect labelR
c_editor->Append(_L("default"), *get_default_extruder_color_icon()); c_editor->Append(_L("default"), *get_default_extruder_color_icon());
for (size_t i = 0; i < icons.size(); i++) 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()) int selection = (has_default_extruder && has_default_extruder())
c_editor->SetSelection(atoi(data.GetText().c_str())); ? atoi(data.GetText().c_str())
else : atoi(data.GetText().c_str()) - 1;
c_editor->SetSelection(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) { c_editor->Bind(wxEVT_SET_FOCUS, [c_editor](wxFocusEvent& evt) {
#ifdef __WXGTK__ #ifdef __WXGTK__
+19 -1
View File
@@ -2561,7 +2561,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
if (!model_volume.is_model_part()) if (!model_volume.is_model_part())
continue; 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); 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()]); 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 // BBS
if (printer_technology == ptFFF && m_config->has("filament_colour") && (m_canvas_type != ECanvasType::CanvasAssembleView)) { if (printer_technology == ptFFF && m_config->has("filament_colour") && (m_canvas_type != ECanvasType::CanvasAssembleView)) {
// Should the wipe tower be visualized ? // Should the wipe tower be visualized ?
+6
View File
@@ -75,6 +75,7 @@
#include "libslic3r/miniz_extension.hpp" #include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "libslic3r/Color.hpp" #include "libslic3r/Color.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "GUI.hpp" #include "GUI.hpp"
#include "GUI_Utils.hpp" #include "GUI_Utils.hpp"
@@ -2950,6 +2951,11 @@ bool GUI_App::on_init_inner()
// BBS if load user preset failed // BBS if load user preset failed
//if (loaded_preset_result != 0) { //if (loaded_preset_result != 0) {
try { 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. // 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 // 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. // installation of a compatible system preset, thus nullifying the system preset substitutions.
+68 -50
View File
@@ -35,9 +35,53 @@ static PrinterTechnology printer_technology()
return wxGetApp().preset_bundle->printers.get_selected_preset().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() 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) 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); menu->Destroy(item_id);
} }
const int filaments_cnt = filaments_count(); // Use UI-ordered filament IDs (physical first, then enabled mixed in UI order)
if (filaments_cnt <= 1) const std::vector<unsigned int> ordered_filament_ids = ui_ordered_filament_ids();
if (ordered_filament_ids.size() <= 1)
return; return;
wxDataViewItemArray sels; wxDataViewItemArray sels;
@@ -1059,24 +1104,16 @@ void MenuFactory::append_menu_item_change_extruder(wxMenu* menu)
if (sels.Count() == 1) { if (sels.Count() == 1) {
const ModelConfig& config = obj_list()->get_item_config(sels[0]); const ModelConfig& config = obj_list()->get_item_config(sels[0]);
// BBS: set default extruder to 1 // 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; const int actual_filament_id = display_idx == 0 ? 0 : int(ordered_filament_ids[display_idx - 1]);
int icon_idx = i == 0 ? 0 : i - 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"); wxString item_name = filament_menu_item_name(actual_filament_id);
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));
}
}
if (is_active_extruder) { if (is_active_extruder) {
item_name << " (" + _L("current") + ")"; 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()) { if (icon_idx >= 0 && icon_idx < icons.size()) {
append_menu_item( 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); [is_active_extruder]() { return !is_active_extruder; }, m_parent);
} else { } else {
append_menu_item( 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); [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); menu->Destroy(item_id);
} }
int filaments_cnt = filaments_count(); const std::vector<unsigned int> ordered_filament_ids = ui_ordered_filament_ids();
if (filaments_cnt <= 1) if (ordered_filament_ids.size() <= 1)
return; return;
wxDataViewItemArray sels; wxDataViewItemArray sels;
@@ -2229,12 +2268,10 @@ void MenuFactory::append_menu_item_change_filament(wxMenu* menu)
} }
std::vector<wxBitmap*> icons = get_extruder_color_icons(true); std::vector<wxBitmap*> icons = get_extruder_color_icons(true);
if (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()%filaments_cnt; BOOST_LOG_TRIVIAL(warning) << boost::format("Warning: icons size %1%, filaments_cnt=%2%") % icons.size() % ordered_filament_ids.size();
if (icons.size() <= 1) if (icons.size() <= 1)
return; return;
else
filaments_cnt = icons.size();
} }
wxMenu* extruder_selection_menu = new wxMenu(); wxMenu* extruder_selection_menu = new wxMenu();
const wxString& name = sels.Count() == 1 ? names[0] : names[1]; 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 int initial_extruder = -1; // negative value for multiple object/part selection
if (sels.Count() == 1) { if (sels.Count() == 1) {
const ModelConfig& config = obj_list()->get_item_config(sels[0]); const ModelConfig& config = obj_list()->get_item_config(sels[0]);
// BBS
const auto sel_vol = obj_list()->get_selected_model_volume(); const auto sel_vol = obj_list()->get_selected_model_volume();
if (sel_vol && sel_vol->type() == ModelVolumeType::PARAMETER_MODIFIER) if (sel_vol && sel_vol->type() == ModelVolumeType::PARAMETER_MODIFIER)
initial_extruder = config.has("extruder") ? config.extruder() : 0; initial_extruder = config.has("extruder") ? config.extruder() : 0;
else else
initial_extruder = config.has("extruder") ? config.extruder() : 1; initial_extruder = config.has("extruder") ? config.extruder() : 0;
} }
// BBS for (size_t display_idx = 0; display_idx <= ordered_filament_ids.size(); ++display_idx)
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++)
{ {
// BBS const int actual_filament_id = display_idx == 0 ? 0 : int(ordered_filament_ids[display_idx - 1]);
//bool is_active_extruder = i == initial_extruder;
bool is_active_extruder = false; bool is_active_extruder = false;
wxString item_name = _L("Default"); wxString item_name = filament_menu_item_name(actual_filament_id);
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));
}
}
if (is_active_extruder) { if (is_active_extruder) {
item_name << " (" + _L("current") + ")"; item_name << " (" + _L("current") + ")";
} }
append_menu_item(extruder_selection_menu, wxID_ANY, item_name, "", 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); [is_active_extruder]() { return !is_active_extruder; }, m_parent);
} }
menu->Append(wxID_ANY, name, extruder_selection_menu, _L("Change Filament")); menu->Append(wxID_ANY, name, extruder_selection_menu, _L("Change Filament"));
+23 -8
View File
@@ -1,4 +1,5 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/PresetBundle.hpp" #include "libslic3r/PresetBundle.hpp"
#include "GUI_ObjectList.hpp" #include "GUI_ObjectList.hpp"
#include "GUI_Factories.hpp" #include "GUI_Factories.hpp"
@@ -75,9 +76,20 @@ static DynamicPrintConfig& printer_config()
return wxGetApp().preset_bundle->printers.get_edited_preset().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() 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) 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) if (printer_technology() == ptSLA)
filaments_count = 1; filaments_count = 1;
const size_t total_filaments = total_filaments_count(filaments_count);
m_prevent_update_filament_in_config = true; 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) if (m_objects)
update_filament_values_for_items(filaments_count); update_filament_values_for_items(total_filaments);
update_filament_colors(); update_filament_colors();
// set show/hide for this column // 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 //a workaround for a wrong last column width updating under OSX
auto em = em_unit(this); auto em = em_unit(this);
GetColumn(colEditing)->SetWidth(m_columns_width[colEditing]*em); 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) 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; 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 // BBS: update extruder values even when filaments_count is 1, because it may be reduced from value greater than 1
if (m_objects) if (m_objects)
@@ -1028,7 +1043,7 @@ void ObjectList::update_objects_list_filament_column_when_delete_filament(size_t
update_filament_colors(); update_filament_colors();
// set show/hide for this column // 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 // a workaround for a wrong last column width updating under OSX
GetColumn(colEditing)->SetWidth(25); GetColumn(colEditing)->SetWidth(25);
@@ -6536,9 +6551,9 @@ void ObjectList::OnEditingDone(wxDataViewEvent &event)
// BBS: remove "const" qualifier // BBS: remove "const" qualifier
void ObjectList::set_extruder_for_selected_items(const int extruder) void ObjectList::set_extruder_for_selected_items(const int extruder)
{ {
// BBS: check extruder id // Accept any configured filament id, including mixed virtual filaments.
std::vector<std::string> colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(); const int max_filaments = filaments_count();
if (extruder > colors.size()) if (extruder < 0 || extruder > max_filaments)
return; return;
wxDataViewItemArray sels; wxDataViewItemArray sels;
+42 -28
View File
@@ -2804,55 +2804,69 @@ int ObjectTablePanel::init_bitmap()
m_undo_bitmap = create_scaled_bitmap("lock_normal", nullptr, 18); m_undo_bitmap = create_scaled_bitmap("lock_normal", nullptr, 18);
m_color_bitmaps = get_extruder_color_icons(); 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; return 0;
} }
int ObjectTablePanel::init_filaments_and_colors() 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; 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) { if (m_filaments_count <= 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") % m_filaments_count; BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", can not get filaments, count: %1%, set to default") % m_filaments_count;
set_default_filaments_and_colors(); set_default_filaments_and_colors();
return -1; 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_colors.resize(m_filaments_count);
m_filaments_name.resize(m_filaments_count); m_filaments_name.resize(m_filaments_count);
unsigned int color_count = filament_opt->values.size(); const size_t physical_count = filament_presets.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;
ColorRGB rgb; ColorRGB rgb;
while (i < m_filaments_count) {
const std::string& txt_color = global_config->opt_string("filament_colour", i); for (int i = 0; i < (int)m_filaments_count; ++i) {
if (i < color_count) { if (size_t(i) < filament_colors.size() && decode_color(filament_colors[size_t(i)], rgb))
if (decode_color(txt_color, rgb))
{
m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar()); m_filaments_colors[i] = wxColour(rgb.r_uchar(), rgb.g_uchar(), rgb.b_uchar());
}
else else
{
m_filaments_colors[i] = *wxGREEN;
}
}
else {
m_filaments_colors[i] = *wxGREEN; 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 // Mixed-slot row: walk the manager and find the (physical_count + offset)-th enabled, non-deleted entry.
m_filaments_name[i] = wxString(std::to_string(i+1) + ": " + filament_presets[i]); 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; return 0;
@@ -179,13 +179,14 @@ void GLGizmoMmuSegmentation::data_changed(bool is_serializing)
ModelObject* model_object = m_c->selection_info()->model_object(); ModelObject* model_object = m_c->selection_info()->model_object();
int prev_extruders_count = int(m_extruders_colors.size()); int prev_extruders_count = int(m_extruders_colors.size());
if (prev_extruders_count != wxGetApp().filaments_cnt()) { int cur_filaments_count = int(wxGetApp().preset_bundle->total_filament_count());
if (wxGetApp().filaments_cnt() > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT)) if (prev_extruders_count != cur_filaments_count) {
if (cur_filaments_count > int(GLGizmoMmuSegmentation::EXTRUDERS_LIMIT))
show_notification_extruders_limit_exceeded(); show_notification_extruders_limit_exceeded();
this->init_extruders_data(); this->init_extruders_data();
// Reinitialize triangle selectors because of change of extruder count need also change the size of GLIndexedVertexArray // 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(); this->init_model_triangle_selectors();
} else if (wxGetApp().plater()->get_extruders_colors() != m_extruders_colors) { } else if (wxGetApp().plater()->get_extruders_colors() != m_extruders_colors) {
this->init_extruders_data(); 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; 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). // ORCA: Remap filaments section (Border only, Title in border).
// Styled as a panel for visual grouping. // Styled as a panel for visual grouping.
@@ -731,6 +733,8 @@ void GLGizmoMmuSegmentation::init_model_triangle_selectors()
continue; continue;
int extruder_idx = (mv->extruder_id() > 0) ? mv->extruder_id() - 1 : 0; 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; std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[size_t(extruder_idx)]); 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()); 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()); TriangleSelectorPatch* selector = dynamic_cast<TriangleSelectorPatch*>(m_triangle_selectors[i].get());
int extruder_idx = m_volumes_extruder_idxs[i]; int extruder_idx = m_volumes_extruder_idxs[i];
int extruder_color_idx = std::max(0, extruder_idx - 1); 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; std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(m_extruders_colors[extruder_color_idx]); ebt_colors.push_back(m_extruders_colors[extruder_color_idx]);
ebt_colors.insert(ebt_colors.end(), m_extruders_colors.begin(), m_extruders_colors.end()); 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 // 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. // using colors from loaded 3MF and not from printer profile in Slicer.
if (int prev_extruders_count = int(m_extruders_colors.size()); 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_extruders_data();
this->init_model_triangle_selectors(); this->init_model_triangle_selectors();
@@ -72,11 +72,8 @@ public:
void data_changed(bool is_serializing) override; void data_changed(bool is_serializing) override;
// TriangleSelector::serialization/deserialization has a limit to store 19 different states. // Keep the paint gizmo limit aligned with TriangleSelector state capacity.
// EXTRUDER_LIMIT + 1 states are used to storing the painting because also uncolored triangles are stored. static const constexpr size_t EXTRUDERS_LIMIT = static_cast<size_t>(EnforcerBlockerType::ExtruderMax);
// 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;
const float get_cursor_radius_min() const override { return CursorRadiusMin; } const float get_cursor_radius_min() const override { return CursorRadiusMin; }
+37 -8
View File
@@ -998,16 +998,42 @@ bool GLGizmosManager::on_key(wxKeyEvent& evt)
keyCode = keyCode- WXK_NUMPAD0+'0'; keyCode = keyCode- WXK_NUMPAD0+'0';
} }
if (keyCode >= '0' && keyCode <= '9') { 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); m_timer_set_color.StartOnce(500);
processed = true; 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 { } else {
processed = mmu_seg->on_number_key_down(keyCode - '0'); 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') { 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) 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()); 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_parent.set_as_dirty();
} }
} }
m_pending_color_shortcut_tens = 0;
}
void GLGizmosManager::update_after_undo_redo(const UndoRedo::Snapshot& snapshot) 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 //When there are more than 9 colors, shortcut key coloring
wxTimer m_timer_set_color; wxTimer m_timer_set_color;
int m_pending_color_shortcut_tens = 0;
void on_set_color_timer(wxTimerEvent& evt); void on_set_color_timer(wxTimerEvent& evt);
// key MENU_ICON_NAME, value = ImtextureID // key MENU_ICON_NAME, value = ImtextureID
+3
View File
@@ -2221,6 +2221,9 @@ bool MainFrame::get_enable_slice_status()
} }
} }
if (enable && m_plater->sidebar().has_broken_mixed_filament())
enable = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": m_slice_select %1%, enable= %2% ")%m_slice_select %enable;
return enable; return enable;
} }
@@ -0,0 +1,810 @@
// MixedFilamentColorMapPanel.cpp
// Extracted verbatim from FullSpectrum Plater.cpp:3087-3781 (Task 17).
#include "MixedFilamentColorMapPanel.hpp"
#include "libslic3r/filament_mixer.h"
#include <wx/dcbuffer.h>
#include <wx/event.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <numeric>
#include <vector>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helpers — free functions used only by this widget.
// Copied verbatim from FullSpectrum Plater.cpp:2443-2640.
// ---------------------------------------------------------------------------
namespace {
wxColour blend_multi_filament_mixer(const std::vector<wxColour> &colors, const std::vector<double> &weights)
{
if (colors.empty() || weights.empty())
return wxColour("#26A69A");
unsigned char out_r = 0;
unsigned char out_g = 0;
unsigned char out_b = 0;
double accumulated_weight = 0.0;
bool has_color = false;
for (size_t i = 0; i < colors.size() && i < weights.size(); ++i) {
const double weight = std::max(0.0, weights[i]);
if (weight <= 0.0)
continue;
const wxColour safe = colors[i].IsOk() ? colors[i] : wxColour("#26A69A");
const unsigned char r = static_cast<unsigned char>(safe.Red());
const unsigned char g = static_cast<unsigned char>(safe.Green());
const unsigned char b = static_cast<unsigned char>(safe.Blue());
if (!has_color) {
out_r = r;
out_g = g;
out_b = b;
accumulated_weight = weight;
has_color = true;
continue;
}
const double new_total = accumulated_weight + weight;
if (new_total <= 0.0)
continue;
const float t = float(weight / new_total);
::Slic3r::filament_mixer_lerp(out_r, out_g, out_b, r, g, b, t, &out_r, &out_g, &out_b);
accumulated_weight = new_total;
}
if (!has_color)
return wxColour("#26A69A");
return wxColour(out_r, out_g, out_b);
}
std::vector<int> normalize_color_match_weights(const std::vector<int> &weights, size_t count)
{
std::vector<int> out = weights;
if (out.size() != count)
out.assign(count, count > 0 ? int(100 / int(count)) : 0);
int sum = 0;
for (int &value : out) {
value = std::max(0, value);
sum += value;
}
if (sum <= 0 && count > 0) {
out.assign(count, 0);
out[0] = 100;
return out;
}
std::vector<double> remainders(count, 0.0);
int assigned = 0;
for (size_t idx = 0; idx < count; ++idx) {
const double exact = 100.0 * double(out[idx]) / double(sum);
out[idx] = int(std::floor(exact));
remainders[idx] = exact - double(out[idx]);
assigned += out[idx];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_remainder = -1.0;
for (size_t idx = 0; idx < remainders.size(); ++idx) {
if (remainders[idx] > best_remainder) {
best_remainder = remainders[idx];
best_idx = idx;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
bool color_match_raw_weights_within_range(const std::vector<double> &weights, int min_component_percent)
{
if (min_component_percent <= 0)
return true;
const double min_allowed = double(std::clamp(min_component_percent, 0, 50));
int active_components = 0;
for (const double weight : weights) {
if (weight <= 1e-4)
continue;
++active_components;
if (weight * 100.0 + 1e-6 < min_allowed)
return false;
}
return active_components >= 2;
}
} // anonymous namespace
// ===========================================================================
// MixedFilamentColorMapPanel — implementation
// Verbatim from FullSpectrum Plater.cpp:3087-3781.
// ===========================================================================
MixedFilamentColorMapPanel::MixedFilamentColorMapPanel(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights,
const wxSize &min_size)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, min_size, wxBORDER_SIMPLE)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(min_size);
m_render_timer.SetOwner(this);
m_colors.reserve(filament_ids.size());
for (const unsigned int filament_id : filament_ids) {
if (filament_id >= 1 && filament_id <= palette.size())
m_colors.emplace_back(palette[filament_id - 1]);
else
m_colors.emplace_back(wxColour("#26A69A"));
}
if (m_colors.empty())
m_colors.emplace_back(wxColour("#26A69A"));
set_normalized_weights(initial_weights, false);
Bind(wxEVT_PAINT, &MixedFilamentColorMapPanel::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &MixedFilamentColorMapPanel::on_left_down, this);
Bind(wxEVT_LEFT_UP, &MixedFilamentColorMapPanel::on_left_up, this);
Bind(wxEVT_MOTION, &MixedFilamentColorMapPanel::on_mouse_move, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedFilamentColorMapPanel::on_capture_lost, this);
Bind(wxEVT_SIZE, &MixedFilamentColorMapPanel::on_size, this);
Bind(wxEVT_TIMER, &MixedFilamentColorMapPanel::on_render_timer, this, m_render_timer.GetId());
}
MixedFilamentColorMapPanel::~MixedFilamentColorMapPanel()
{
if (HasCapture())
ReleaseMouse();
if (m_render_timer.IsRunning())
m_render_timer.Stop();
}
std::vector<int> MixedFilamentColorMapPanel::normalized_weights() const
{
return m_weights;
}
wxColour MixedFilamentColorMapPanel::selected_color() const
{
std::vector<double> weights;
weights.reserve(m_weights.size());
for (const int weight : m_weights)
weights.emplace_back(double(std::max(0, weight)));
return blend_multi_filament_mixer(m_colors, weights);
}
void MixedFilamentColorMapPanel::set_normalized_weights(const std::vector<int> &weights, bool notify)
{
m_weights = normalize_color_match_weights(weights, m_colors.size());
initialize_cursor_from_weights();
Refresh();
if (notify)
emit_changed();
}
void MixedFilamentColorMapPanel::set_min_component_percent(int min_component_percent)
{
const int clamped = std::clamp(min_component_percent, 0, 50);
if (m_min_component_percent == clamped)
return;
m_min_component_percent = clamped;
invalidate_cached_bitmap();
Refresh();
}
// ---------------------------------------------------------------------------
// Private: geometry helpers
// ---------------------------------------------------------------------------
MixedFilamentColorMapPanel::GeometryMode MixedFilamentColorMapPanel::geometry_mode() const
{
if (m_colors.size() <= 1)
return GeometryMode::Point;
if (m_colors.size() == 2)
return GeometryMode::Line;
if (m_colors.size() == 3)
return GeometryMode::Triangle;
if (m_colors.size() == 4)
return GeometryMode::TriangleWithCenter;
return GeometryMode::Radial;
}
wxRect MixedFilamentColorMapPanel::canvas_rect() const
{
const wxSize size = GetClientSize();
return wxRect(0, 0, std::max(1, size.GetWidth()), std::max(1, size.GetHeight()));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::make_vec(double x, double y)
{
return Vec2 { x, y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::add_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return Vec2 { lhs.x + rhs.x, lhs.y + rhs.y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::sub_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return Vec2 { lhs.x - rhs.x, lhs.y - rhs.y };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::scale_vec(const Vec2 &value, double factor)
{
return Vec2 { value.x * factor, value.y * factor };
}
double MixedFilamentColorMapPanel::dot_vec(const Vec2 &lhs, const Vec2 &rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y;
}
double MixedFilamentColorMapPanel::length_sq(const Vec2 &value)
{
return dot_vec(value, value);
}
double MixedFilamentColorMapPanel::dist_sq(const Vec2 &lhs, const Vec2 &rhs)
{
return length_sq(sub_vec(lhs, rhs));
}
std::array<MixedFilamentColorMapPanel::Vec2, 3> MixedFilamentColorMapPanel::simplex_vertices() const
{
return { make_vec(0.50, 0.05), make_vec(0.08, 0.94), make_vec(0.92, 0.94) };
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::simplex_center() const
{
const auto vertices = simplex_vertices();
return make_vec((vertices[0].x + vertices[1].x + vertices[2].x) / 3.0,
(vertices[0].y + vertices[1].y + vertices[2].y) / 3.0);
}
std::vector<MixedFilamentColorMapPanel::AnchorPoint> MixedFilamentColorMapPanel::radial_anchor_points() const
{
std::vector<AnchorPoint> anchors;
const size_t count = m_colors.size();
anchors.reserve(count);
if (count == 0)
return anchors;
if (count == 1) {
anchors.emplace_back(AnchorPoint { 0.5, 0.5 });
return anchors;
}
if (count == 2) {
anchors.emplace_back(AnchorPoint { 0.0, 0.5 });
anchors.emplace_back(AnchorPoint { 1.0, 0.5 });
return anchors;
}
if (count == 3) {
anchors.emplace_back(AnchorPoint { 0.0, 0.5 });
anchors.emplace_back(AnchorPoint { 1.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 1.0 });
return anchors;
}
if (count == 4) {
anchors.emplace_back(AnchorPoint { 0.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 0.0 });
anchors.emplace_back(AnchorPoint { 1.0, 1.0 });
anchors.emplace_back(AnchorPoint { 0.0, 1.0 });
return anchors;
}
constexpr double k_pi = 3.14159265358979323846;
const double center_x = 0.5;
const double center_y = 0.5;
const double radius = 0.45;
for (size_t idx = 0; idx < count; ++idx) {
const double angle = (2.0 * k_pi * double(idx)) / double(count);
anchors.emplace_back(AnchorPoint { center_x + radius * std::cos(angle), center_y + radius * std::sin(angle) });
}
return anchors;
}
std::vector<MixedFilamentColorMapPanel::AnchorPoint> MixedFilamentColorMapPanel::anchor_points() const
{
std::vector<AnchorPoint> anchors;
switch (geometry_mode()) {
case GeometryMode::Point:
anchors.emplace_back(AnchorPoint { 0.5, 0.5 });
break;
case GeometryMode::Line:
anchors.emplace_back(AnchorPoint { 0.06, 0.5 });
anchors.emplace_back(AnchorPoint { 0.94, 0.5 });
break;
case GeometryMode::Triangle: {
const auto vertices = simplex_vertices();
for (const Vec2 &vertex : vertices)
anchors.emplace_back(AnchorPoint { vertex.x, vertex.y });
break;
}
case GeometryMode::TriangleWithCenter: {
const auto vertices = simplex_vertices();
for (const Vec2 &vertex : vertices)
anchors.emplace_back(AnchorPoint { vertex.x, vertex.y });
const Vec2 center = simplex_center();
anchors.emplace_back(AnchorPoint { center.x, center.y });
break;
}
case GeometryMode::Radial:
anchors = radial_anchor_points();
break;
}
return anchors;
}
std::array<double, 3> MixedFilamentColorMapPanel::triangle_barycentric(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
const Vec2 &a = triangle[0];
const Vec2 &b = triangle[1];
const Vec2 &c = triangle[2];
const double denom = ((b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y));
if (std::abs(denom) <= 1e-9)
return { 1.0, 0.0, 0.0 };
const double w0 = ((b.y - c.y) * (point.x - c.x) + (c.x - b.x) * (point.y - c.y)) / denom;
const double w1 = ((c.y - a.y) * (point.x - c.x) + (a.x - c.x) * (point.y - c.y)) / denom;
const double w2 = 1.0 - w0 - w1;
return { w0, w1, w2 };
}
bool MixedFilamentColorMapPanel::point_in_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
const auto barycentric = triangle_barycentric(point, triangle);
constexpr double eps = 1e-6;
return barycentric[0] >= -eps && barycentric[1] >= -eps && barycentric[2] >= -eps;
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::closest_point_on_segment(const Vec2 &point, const Vec2 &start, const Vec2 &end)
{
const Vec2 edge = sub_vec(end, start);
const double edge_len_sq = length_sq(edge);
if (edge_len_sq <= 1e-9)
return start;
const double t = std::clamp(dot_vec(sub_vec(point, start), edge) / edge_len_sq, 0.0, 1.0);
return add_vec(start, scale_vec(edge, t));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::closest_point_on_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle)
{
if (point_in_triangle(point, triangle))
return point;
Vec2 best = triangle[0];
double best_dist = std::numeric_limits<double>::max();
for (int edge_idx = 0; edge_idx < 3; ++edge_idx) {
const Vec2 candidate = closest_point_on_segment(point, triangle[edge_idx], triangle[(edge_idx + 1) % 3]);
const double candidate_dist = dist_sq(point, candidate);
if (candidate_dist < best_dist) {
best_dist = candidate_dist;
best = candidate;
}
}
return best;
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::normalized_point_from_mouse(const wxMouseEvent &evt) const
{
const wxRect rect = canvas_rect();
const int width = std::max(1, rect.GetWidth() - 1);
const int height = std::max(1, rect.GetHeight() - 1);
return make_vec(
std::clamp(double(evt.GetX() - rect.GetLeft()) / double(width), 0.0, 1.0),
std::clamp(double(evt.GetY() - rect.GetTop()) / double(height), 0.0, 1.0));
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::clamp_point_to_geometry(const Vec2 &point) const
{
switch (geometry_mode()) {
case GeometryMode::Point:
return make_vec(0.5, 0.5);
case GeometryMode::Line:
return make_vec(std::clamp(point.x, 0.0, 1.0), 0.5);
case GeometryMode::Triangle:
case GeometryMode::TriangleWithCenter:
return closest_point_on_triangle(point, simplex_vertices());
case GeometryMode::Radial:
return make_vec(std::clamp(point.x, 0.0, 1.0), std::clamp(point.y, 0.0, 1.0));
}
return point;
}
std::vector<double> MixedFilamentColorMapPanel::simplex_weights_from_pos(const Vec2 &point) const
{
const auto triangle = simplex_vertices();
const Vec2 clamped = closest_point_on_triangle(point, triangle);
const auto barycentric = triangle_barycentric(clamped, triangle);
if (geometry_mode() == GeometryMode::Triangle)
return { std::max(0.0, barycentric[0]), std::max(0.0, barycentric[1]), std::max(0.0, barycentric[2]) };
const double shared = std::max(0.0, std::min({ barycentric[0], barycentric[1], barycentric[2] }));
return {
std::max(0.0, barycentric[0] - shared),
std::max(0.0, barycentric[1] - shared),
std::max(0.0, barycentric[2] - shared),
std::max(0.0, shared * 3.0)
};
}
MixedFilamentColorMapPanel::Vec2 MixedFilamentColorMapPanel::triangle_point_from_weights() const
{
const auto vertices = simplex_vertices();
double total = 0.0;
for (size_t idx = 0; idx < 3 && idx < m_weights.size(); ++idx)
total += std::max(0, m_weights[idx]);
if (total <= 0.0)
return simplex_center();
Vec2 out = make_vec(0.0, 0.0);
for (size_t idx = 0; idx < 3 && idx < m_weights.size(); ++idx) {
const double weight = double(std::max(0, m_weights[idx])) / total;
out = add_vec(out, scale_vec(vertices[idx], weight));
}
return out;
}
void MixedFilamentColorMapPanel::initialize_cursor_from_grid_search()
{
double best_x = 0.5;
double best_y = 0.5;
double best_error = std::numeric_limits<double>::max();
constexpr int grid = 96;
for (int y_idx = 0; y_idx <= grid; ++y_idx) {
for (int x_idx = 0; x_idx <= grid; ++x_idx) {
const Vec2 point = clamp_point_to_geometry(make_vec(double(x_idx) / double(grid), double(y_idx) / double(grid)));
const std::vector<int> probe = normalized_weights_from_pos(point.x, point.y);
if (probe.size() != m_weights.size())
continue;
double error = 0.0;
for (size_t idx = 0; idx < probe.size(); ++idx) {
const double delta = double(probe[idx] - m_weights[idx]);
error += delta * delta;
}
if (error < best_error) {
best_error = error;
best_x = point.x;
best_y = point.y;
}
}
}
m_cursor_x = best_x;
m_cursor_y = best_y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
}
std::vector<double> MixedFilamentColorMapPanel::raw_weights_from_pos(double normalized_x, double normalized_y) const
{
switch (geometry_mode()) {
case GeometryMode::Point:
return { 1.0 };
case GeometryMode::Line: {
const double t = std::clamp(normalized_x, 0.0, 1.0);
return { 1.0 - t, t };
}
case GeometryMode::Triangle:
case GeometryMode::TriangleWithCenter:
return simplex_weights_from_pos(make_vec(normalized_x, normalized_y));
case GeometryMode::Radial:
break;
}
const std::vector<AnchorPoint> anchors = radial_anchor_points();
std::vector<double> out(anchors.size(), 0.0);
if (anchors.empty())
return out;
constexpr double eps = 1e-8;
size_t exact_idx = size_t(-1);
for (size_t idx = 0; idx < anchors.size(); ++idx) {
const double dx = normalized_x - anchors[idx].x;
const double dy = normalized_y - anchors[idx].y;
const double d2 = dx * dx + dy * dy;
if (d2 <= eps) {
exact_idx = idx;
break;
}
out[idx] = 1.0 / std::max(1e-6, d2);
}
if (exact_idx != size_t(-1)) {
std::fill(out.begin(), out.end(), 0.0);
out[exact_idx] = 1.0;
return out;
}
double sum = 0.0;
for (const double value : out)
sum += value;
if (sum <= 0.0) {
out.assign(out.size(), 0.0);
out[0] = 1.0;
return out;
}
for (double &value : out)
value /= sum;
return out;
}
std::vector<int> MixedFilamentColorMapPanel::normalized_weights_from_pos(double normalized_x, double normalized_y) const
{
std::vector<int> raw_weights;
const std::vector<double> raw = raw_weights_from_pos(normalized_x, normalized_y);
raw_weights.reserve(raw.size());
for (const double value : raw)
raw_weights.emplace_back(std::max(0, int(std::lround(value * 100.0))));
return normalize_color_match_weights(raw_weights, raw.size());
}
void MixedFilamentColorMapPanel::initialize_cursor_from_weights()
{
if (m_weights.empty()) {
m_cursor_x = 0.5;
m_cursor_y = 0.5;
return;
}
switch (geometry_mode()) {
case GeometryMode::Point:
m_cursor_x = 0.5;
m_cursor_y = 0.5;
break;
case GeometryMode::Line: {
const int total = std::accumulate(m_weights.begin(), m_weights.end(), 0);
const double t = total > 0 && m_weights.size() >= 2 ? double(std::max(0, m_weights[1])) / double(total) : 0.5;
m_cursor_x = std::clamp(t, 0.0, 1.0);
m_cursor_y = 0.5;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
break;
}
case GeometryMode::Triangle: {
const Vec2 point = triangle_point_from_weights();
m_cursor_x = point.x;
m_cursor_y = point.y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
break;
}
case GeometryMode::TriangleWithCenter:
case GeometryMode::Radial:
initialize_cursor_from_grid_search();
break;
}
}
// ---------------------------------------------------------------------------
// Private: interaction + rendering
// ---------------------------------------------------------------------------
void MixedFilamentColorMapPanel::emit_changed()
{
wxCommandEvent evt(wxEVT_SLIDER, GetId());
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
void MixedFilamentColorMapPanel::update_from_mouse(const wxMouseEvent &evt, bool notify)
{
const Vec2 point = clamp_point_to_geometry(normalized_point_from_mouse(evt));
m_cursor_x = point.x;
m_cursor_y = point.y;
m_weights = normalized_weights_from_pos(m_cursor_x, m_cursor_y);
Refresh();
if (notify)
emit_changed();
}
wxColour MixedFilamentColorMapPanel::canvas_background_color() const
{
return GetBackgroundColour().IsOk() ? GetBackgroundColour() : wxColour(245, 245, 245);
}
bool MixedFilamentColorMapPanel::cached_bitmap_matches(const wxSize &size, const wxColour &background) const
{
return m_cached_bitmap.IsOk() && m_cached_bitmap_size == size && m_cached_background == background;
}
void MixedFilamentColorMapPanel::schedule_cached_bitmap_render()
{
if (!m_render_timer.IsRunning())
m_render_timer.StartOnce(80);
}
void MixedFilamentColorMapPanel::invalidate_cached_bitmap()
{
m_cached_bitmap = wxBitmap();
m_cached_bitmap_size = wxSize();
m_cached_background = wxColour();
}
void MixedFilamentColorMapPanel::render_cached_bitmap(const wxSize &size, const wxColour &background)
{
const int width = size.GetWidth();
const int height = size.GetHeight();
if (width <= 0 || height <= 0)
return;
wxImage image(width, height);
unsigned char *data = image.GetData();
if (data != nullptr) {
for (int y = 0; y < height; ++y) {
const double normalized_y = (height > 1) ? double(y) / double(height - 1) : 0.5;
for (int x = 0; x < width; ++x) {
const double normalized_x = (width > 1) ? double(x) / double(width - 1) : 0.5;
const int data_idx = (y * width + x) * 3;
bool paint_pixel = true;
if (geometry_mode() == GeometryMode::Triangle || geometry_mode() == GeometryMode::TriangleWithCenter)
paint_pixel = point_in_triangle(make_vec(normalized_x, normalized_y), simplex_vertices());
const std::vector<double> raw_weights = raw_weights_from_pos(normalized_x, normalized_y);
wxColour color = paint_pixel ? blend_multi_filament_mixer(m_colors, raw_weights) : background;
if (paint_pixel && m_min_component_percent > 0 &&
!color_match_raw_weights_within_range(raw_weights, m_min_component_percent)) {
const bool stripe = (((x + y) / 8) % 2) == 0;
const double factor = stripe ? 0.12 : 0.38;
color = wxColour(
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Red()) * factor)), 0, 255)),
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Green()) * factor)), 0, 255)),
static_cast<unsigned char>(std::clamp(int(std::lround(double(color.Blue()) * factor)), 0, 255)));
}
data[data_idx + 0] = color.Red();
data[data_idx + 1] = color.Green();
data[data_idx + 2] = color.Blue();
}
}
}
m_cached_bitmap = wxBitmap(image);
m_cached_bitmap_size = size;
m_cached_background = background;
}
void MixedFilamentColorMapPanel::draw_cached_bitmap(wxAutoBufferedPaintDC &dc, const wxRect &rect)
{
if (!m_cached_bitmap.IsOk())
return;
if (m_cached_bitmap_size == rect.GetSize()) {
dc.DrawBitmap(m_cached_bitmap, rect.GetLeft(), rect.GetTop(), false);
return;
}
wxMemoryDC memdc;
memdc.SelectObject(m_cached_bitmap);
dc.StretchBlit(rect.GetLeft(), rect.GetTop(), rect.GetWidth(), rect.GetHeight(),
&memdc, 0, 0, m_cached_bitmap_size.GetWidth(), m_cached_bitmap_size.GetHeight());
memdc.SelectObject(wxNullBitmap);
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void MixedFilamentColorMapPanel::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const wxRect rect = canvas_rect();
const int width = rect.GetWidth();
const int height = rect.GetHeight();
if (width <= 0 || height <= 0)
return;
const wxColour background = canvas_background_color();
if (!cached_bitmap_matches(rect.GetSize(), background)) {
if (!m_cached_bitmap.IsOk())
render_cached_bitmap(rect.GetSize(), background);
else
schedule_cached_bitmap_render();
}
draw_cached_bitmap(dc, rect);
if (geometry_mode() == GeometryMode::Triangle || geometry_mode() == GeometryMode::TriangleWithCenter) {
const auto triangle = simplex_vertices();
wxPoint points[3] = {
wxPoint(rect.GetLeft() + int(std::lround(triangle[0].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[0].y * double(std::max(1, height - 1))))),
wxPoint(rect.GetLeft() + int(std::lround(triangle[1].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[1].y * double(std::max(1, height - 1))))),
wxPoint(rect.GetLeft() + int(std::lround(triangle[2].x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(triangle[2].y * double(std::max(1, height - 1)))))
};
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawPolygon(3, points);
if (geometry_mode() == GeometryMode::TriangleWithCenter) {
const Vec2 center = simplex_center();
const wxPoint center_pt(rect.GetLeft() + int(std::lround(center.x * double(std::max(1, width - 1)))),
rect.GetTop() + int(std::lround(center.y * double(std::max(1, height - 1)))));
dc.SetPen(wxPen(wxColour(180, 180, 180), 1, wxPENSTYLE_DOT));
for (const wxPoint &vertex : points)
dc.DrawLine(center_pt, vertex);
}
} else {
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
}
dc.SetPen(wxPen(wxColour(160, 160, 160), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
const auto anchors = anchor_points();
for (size_t idx = 0; idx < anchors.size() && idx < m_colors.size(); ++idx) {
const int anchor_x = rect.GetLeft() + int(std::lround(anchors[idx].x * double(std::max(1, width - 1))));
const int anchor_y = rect.GetTop() + int(std::lround(anchors[idx].y * double(std::max(1, height - 1))));
dc.SetPen(wxPen(wxColour(30, 30, 30), 1));
dc.SetBrush(wxBrush(m_colors[idx]));
dc.DrawCircle(wxPoint(anchor_x, anchor_y), FromDIP(4));
}
const int cursor_x = rect.GetLeft() + int(std::lround(m_cursor_x * double(std::max(1, width - 1))));
const int cursor_y = rect.GetTop() + int(std::lround(m_cursor_y * double(std::max(1, height - 1))));
dc.SetPen(wxPen(wxColour(255, 255, 255), 3));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawCircle(wxPoint(cursor_x, cursor_y), FromDIP(7));
dc.SetPen(wxPen(wxColour(30, 30, 30), 1));
dc.DrawCircle(wxPoint(cursor_x, cursor_y), FromDIP(7));
}
void MixedFilamentColorMapPanel::on_left_down(wxMouseEvent &evt)
{
if (!HasCapture())
CaptureMouse();
m_dragging = true;
update_from_mouse(evt, true);
}
void MixedFilamentColorMapPanel::on_left_up(wxMouseEvent &evt)
{
if (m_dragging)
update_from_mouse(evt, true);
m_dragging = false;
if (HasCapture())
ReleaseMouse();
}
void MixedFilamentColorMapPanel::on_mouse_move(wxMouseEvent &evt)
{
if (m_dragging && evt.LeftIsDown())
update_from_mouse(evt, true);
}
void MixedFilamentColorMapPanel::on_capture_lost(wxMouseCaptureLostEvent &)
{
m_dragging = false;
}
void MixedFilamentColorMapPanel::on_size(wxSizeEvent &evt)
{
if (m_cached_bitmap.IsOk())
schedule_cached_bitmap_render();
Refresh(false);
evt.Skip();
}
void MixedFilamentColorMapPanel::on_render_timer(wxTimerEvent &)
{
const wxRect rect = canvas_rect();
render_cached_bitmap(rect.GetSize(), canvas_background_color());
Refresh(false);
}
} } // namespace Slic3r::GUI
@@ -0,0 +1,138 @@
#pragma once
#include <wx/panel.h>
#include <wx/timer.h>
#include <wx/bitmap.h>
#include <wx/colour.h>
#include <vector>
#include <array>
#include <functional>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// MixedFilamentColorMapPanel
//
// Interactive colour-map widget that lets the user pick a multi-filament
// blend by dragging a cursor across a geometry-specific gradient map.
//
// Extracted verbatim from FullSpectrum Plater.cpp:3087-3781 (Task 17).
// ---------------------------------------------------------------------------
class MixedFilamentColorMapPanel : public wxPanel
{
public:
MixedFilamentColorMapPanel(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights,
const wxSize &min_size);
~MixedFilamentColorMapPanel() override;
// Returns the current normalised per-filament weights (sum == 100).
std::vector<int> normalized_weights() const;
// Returns the blended wxColour that corresponds to the current cursor position.
wxColour selected_color() const;
// Programmatically update weights; notify==true fires wxEVT_SLIDER.
void set_normalized_weights(const std::vector<int> &weights, bool notify);
// Minimum per-component percentage below which the region is dimmed/striped.
void set_min_component_percent(int min_component_percent);
private:
// -----------------------------------------------------------------------
// Private nested types (verbatim from FullSpectrum Plater.cpp:3160-3180)
// -----------------------------------------------------------------------
enum class GeometryMode {
Point,
Line,
Triangle,
TriangleWithCenter,
Radial
};
struct AnchorPoint {
double x { 0.5 };
double y { 0.5 };
};
struct Vec2 {
double x { 0.0 };
double y { 0.0 };
};
// -----------------------------------------------------------------------
// Geometry helpers (all inlined in .cpp)
// -----------------------------------------------------------------------
GeometryMode geometry_mode() const;
wxRect canvas_rect() const;
static Vec2 make_vec(double x, double y);
static Vec2 add_vec(const Vec2 &lhs, const Vec2 &rhs);
static Vec2 sub_vec(const Vec2 &lhs, const Vec2 &rhs);
static Vec2 scale_vec(const Vec2 &value, double factor);
static double dot_vec(const Vec2 &lhs, const Vec2 &rhs);
static double length_sq(const Vec2 &value);
static double dist_sq(const Vec2 &lhs, const Vec2 &rhs);
std::array<Vec2, 3> simplex_vertices() const;
Vec2 simplex_center() const;
std::vector<AnchorPoint> radial_anchor_points() const;
std::vector<AnchorPoint> anchor_points() const;
static std::array<double, 3> triangle_barycentric(const Vec2 &point, const std::array<Vec2, 3> &triangle);
static bool point_in_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle);
static Vec2 closest_point_on_segment(const Vec2 &point, const Vec2 &start, const Vec2 &end);
static Vec2 closest_point_on_triangle(const Vec2 &point, const std::array<Vec2, 3> &triangle);
Vec2 normalized_point_from_mouse(const wxMouseEvent &evt) const;
Vec2 clamp_point_to_geometry(const Vec2 &point) const;
std::vector<double> simplex_weights_from_pos(const Vec2 &point) const;
Vec2 triangle_point_from_weights() const;
void initialize_cursor_from_grid_search();
std::vector<double> raw_weights_from_pos(double normalized_x, double normalized_y) const;
std::vector<int> normalized_weights_from_pos(double normalized_x, double normalized_y) const;
void initialize_cursor_from_weights();
// -----------------------------------------------------------------------
// Rendering helpers
// -----------------------------------------------------------------------
void emit_changed();
void update_from_mouse(const wxMouseEvent &evt, bool notify);
wxColour canvas_background_color() const;
bool cached_bitmap_matches(const wxSize &size, const wxColour &background) const;
void schedule_cached_bitmap_render();
void invalidate_cached_bitmap();
void render_cached_bitmap(const wxSize &size, const wxColour &background);
void draw_cached_bitmap(wxAutoBufferedPaintDC &dc, const wxRect &rect);
// -----------------------------------------------------------------------
// wx event handlers
// -----------------------------------------------------------------------
void on_paint(wxPaintEvent &evt);
void on_left_down(wxMouseEvent &evt);
void on_left_up(wxMouseEvent &evt);
void on_mouse_move(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
void on_size(wxSizeEvent &evt);
void on_render_timer(wxTimerEvent &evt);
// -----------------------------------------------------------------------
// Member variables (verbatim from FullSpectrum Plater.cpp:3761-3779)
// -----------------------------------------------------------------------
std::vector<wxColour> m_colors;
std::vector<int> m_weights;
wxBitmap m_cached_bitmap;
wxSize m_cached_bitmap_size;
wxColour m_cached_background;
wxTimer m_render_timer;
int m_min_component_percent { 0 };
double m_cursor_x { 0.5 };
double m_cursor_y { 0.5 };
bool m_dragging { false };
};
} } // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
#pragma once
#include <wx/dialog.h>
#include <wx/clrpicker.h>
#include <wx/colour.h>
#include <wx/gauge.h>
#include <wx/scrolwin.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/timer.h>
#include <wx/wrapsizer.h>
#include <limits>
#include <string>
#include <vector>
#include "GUI_Utils.hpp"
#include "libslic3r/MixedFilament.hpp"
namespace Slic3r { namespace GUI {
class MixedFilamentColorMapPanel; // Task 17
// ---------------------------------------------------------------------------
// MixedColorMatchRecipeResult
//
// Verbatim from FullSpectrum Plater.cpp:230-242.
// Holds the result of a brute-force C(N,2)/C(N,3)/C(N,4) ΔE₀₀ search.
// ---------------------------------------------------------------------------
struct MixedColorMatchRecipeResult
{
bool cancelled = false;
bool valid = false;
unsigned int component_a = 1;
unsigned int component_b = 2;
int mix_b_percent = 50;
std::string manual_pattern;
std::string gradient_component_ids;
std::string gradient_component_weights;
wxColour preview_color = wxColour("#26A69A");
double delta_e = std::numeric_limits<double>::infinity();
};
// Free helper (was declared at Plater.cpp:244-246): launch dialog, return recipe.
MixedColorMatchRecipeResult prompt_best_color_match_recipe(
wxWindow *parent,
const std::vector<std::string> &physical_colors,
const wxColour &initial_color);
// Free helper (was declared at Plater.cpp:251):
// build a MixedFilamentDisplayContext from a flat color vector.
MixedFilamentDisplayContext build_mixed_filament_display_context(
const std::vector<std::string> &physical_colors);
// Free helper (was declared in Plater.cpp anon namespace at 252):
// map a recipe to a swatch colour using the display context.
wxColour compute_color_match_recipe_display_color(
const MixedColorMatchRecipeResult &recipe,
const MixedFilamentDisplayContext &context);
// Free helper (was declared at Plater.cpp:247): ΔE₀₀ between two wxColours.
double color_delta_e00(const wxColour &lhs, const wxColour &rhs);
// ---------------------------------------------------------------------------
// MixedFilamentColorMatchDialog
//
// Extracted verbatim from FullSpectrum Plater.cpp:3782-4288.
// The user types/picks an arbitrary target colour and a brute-force search
// finds the C(N,2)/C(N,3)/C(N,4) recipe that minimises ΔE₀₀ to that target.
// ---------------------------------------------------------------------------
class MixedFilamentColorMatchDialog : public DPIDialog
{
public:
MixedFilamentColorMatchDialog(wxWindow *parent,
const std::vector<std::string> &physical_colors,
const wxColour &initial_color);
~MixedFilamentColorMatchDialog() override;
// Kick off the initial background recipe search (called after ShowModal starts).
void begin_initial_recipe_load();
MixedColorMatchRecipeResult selected_recipe() const { return m_selected_recipe; }
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
// UI helpers
void update_range_label();
void rebuild_presets_ui();
void set_recipe_loading(bool loading, const wxString &message);
void sync_inputs_to_requested();
bool apply_requested_target(const wxColour &requested_target);
bool apply_hex_input(bool show_invalid_error);
void request_recipe_match(const wxColour &requested_target, bool debounce, const wxString &loading_message);
void refresh_selected_recipe();
void launch_recipe_match(size_t request_token, const wxColour &requested_target);
void update_dialog_state();
// Declared in the task spec's public API
void sync_recipe_preview(MixedColorMatchRecipeResult &recipe, const wxColour *requested_target = nullptr);
void handle_recipe_result(size_t request_token, const wxColour &requested_target, MixedColorMatchRecipeResult recipe);
void apply_preset(MixedColorMatchRecipeResult preset);
// Data
std::vector<std::string> m_physical_colors;
MixedFilamentDisplayContext m_display_context;
std::vector<wxColour> m_palette;
std::vector<MixedColorMatchRecipeResult> m_presets;
MixedFilamentColorMapPanel *m_color_map = nullptr;
// Widgets
wxTextCtrl *m_hex_input = nullptr;
wxColourPickerCtrl *m_classic_picker = nullptr;
wxSlider *m_range_slider = nullptr;
wxStaticText *m_range_value = nullptr;
wxStaticText *m_presets_label = nullptr;
wxScrolledWindow *m_presets_host = nullptr;
wxWrapSizer *m_presets_sizer = nullptr;
wxPanel *m_loading_panel = nullptr;
wxStaticText *m_loading_label = nullptr;
wxGauge *m_loading_gauge = nullptr;
wxPanel *m_selected_preview = nullptr;
wxStaticText *m_selected_label = nullptr;
wxPanel *m_recipe_preview = nullptr;
wxStaticText *m_recipe_label = nullptr;
wxStaticText *m_delta_label = nullptr;
wxStaticText *m_error_label = nullptr;
// State
wxColour m_requested_target { wxColour("#26A69A") };
wxColour m_selected_target { wxColour("#26A69A") };
MixedColorMatchRecipeResult m_selected_recipe;
wxTimer m_recipe_timer;
wxTimer m_loading_timer;
wxString m_loading_message;
size_t m_recipe_request_token { 0 };
int m_min_component_percent { 15 };
bool m_has_recipe_result { false };
bool m_recipe_loading { false };
bool m_recipe_refresh_pending { false };
bool m_syncing_inputs { false };
};
} } // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
#pragma once
#include <wx/panel.h>
#include <wx/checkbox.h>
#include <wx/choice.h>
#include <wx/spinctrl.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <vector>
#include <string>
#include <functional>
#include <memory>
#include "libslic3r/MixedFilament.hpp"
namespace Slic3r { namespace GUI {
class MixedMixPreview; // Task 15
class MixedGradientSelector; // Task 16
// ---------------------------------------------------------------------------
// MixedFilamentConfigPanel
//
// Inline per-row editor for a single MixedFilament entry. Composes
// MixedMixPreview, MixedGradientSelector and MixedGradientWeightsDialog.
//
// Extracted from FullSpectrum Plater.cpp:4588-6857.
// ---------------------------------------------------------------------------
class MixedFilamentConfigPanel : public wxPanel
{
public:
using OnChangeFn = std::function<void(const MixedFilament &)>;
MixedFilamentConfigPanel(wxWindow *parent,
size_t mixed_id,
const MixedFilament &mf,
size_t num_physical,
const std::vector<std::string> &physical_colors,
const std::vector<double> &nozzle_diameters,
const std::vector<wxColour> &palette,
const MixedFilamentPreviewSettings &preview_settings,
bool bias_mode_enabled,
OnChangeFn on_change = {});
// Get the updated mixed filament data.
MixedFilament get_mixed_filament() const { return m_mf; }
bool has_changes() const { return m_has_changes; }
static int effective_local_z_preview_mix_b_percent(const MixedFilament &mf,
const MixedFilamentPreviewSettings &preview_settings);
private:
void build_ui();
void update_preview();
void update_local_z_breakdown();
void update_component_picker_visuals();
// Static helpers — verbatim from FullSpectrum Plater.cpp:4943-6042.
static std::vector<unsigned int> decode_gradient_ids(const std::string &s);
static std::string encode_gradient_ids(const std::vector<unsigned int> &ids);
static std::vector<unsigned int> decode_manual_pattern_ids(const std::string &pattern,
unsigned int a,
unsigned int b,
size_t num_physical,
size_t wall_loops = 0);
static std::vector<int> decode_gradient_weights(const std::string &s, size_t n);
static std::vector<int> normalize_gradient_weights(const std::vector<int> &w, size_t n);
static std::string encode_gradient_weights(const std::vector<int> &w);
static std::vector<unsigned int> build_weighted_pair_sequence(unsigned int a, unsigned int b, int percent_b, bool limit_cycle = false);
static std::vector<unsigned int> build_weighted_multi_sequence(const std::vector<unsigned int> &ids,
const std::vector<int> &weights,
size_t max_cycle_limit = 0);
static std::string summarize_sequence(const std::vector<unsigned int> &seq);
static std::string summarize_local_z_breakdown(const MixedFilament &mf,
const std::vector<int> &weights,
const MixedFilamentPreviewSettings &preview_settings);
static std::string blend_from_sequence(const std::vector<std::string> &colors,
const std::vector<unsigned int> &seq,
const std::string &fallback);
static std::vector<double> build_local_z_preview_pass_heights(double nominal_layer_height,
double lower_bound,
double upper_bound,
double preferred_a_height,
double preferred_b_height,
int mix_b_percent,
int max_sublayers_limit);
size_t m_mixed_id;
MixedFilament m_mf;
size_t m_num_physical;
std::vector<std::string> m_physical_colors;
std::vector<double> m_nozzle_diameters;
std::vector<wxColour> m_palette;
MixedFilamentPreviewSettings m_preview_settings;
bool m_bias_mode_enabled = false;
bool m_has_changes = false;
wxChoice *m_choice_a = nullptr;
wxChoice *m_choice_b = nullptr;
wxChoice *m_choice_c = nullptr;
wxChoice *m_choice_d = nullptr;
wxPanel *m_picker_a_container = nullptr;
wxPanel *m_picker_b_container = nullptr;
wxPanel *m_picker_c_container = nullptr;
wxPanel *m_picker_d_container = nullptr;
wxPanel *m_picker_a_swatch = nullptr;
wxPanel *m_picker_b_swatch = nullptr;
wxPanel *m_picker_c_swatch = nullptr;
wxPanel *m_picker_d_swatch = nullptr;
wxStaticText *m_picker_a_label = nullptr;
wxStaticText *m_picker_b_label = nullptr;
wxStaticText *m_picker_c_label = nullptr;
wxStaticText *m_picker_d_label = nullptr;
wxPanel *m_surface_offset_target_container = nullptr;
wxPanel *m_surface_offset_target_swatch = nullptr;
wxStaticText *m_surface_offset_target_label = nullptr;
MixedGradientSelector *m_blend_selector = nullptr;
wxStaticText *m_blend_label = nullptr;
wxTextCtrl *m_pattern_ctrl = nullptr;
wxCheckBox *m_local_z_limit_checkbox = nullptr;
wxSpinCtrl *m_local_z_limit_spin = nullptr;
wxSpinCtrlDouble *m_surface_offset_spin = nullptr;
std::vector<wxButton *> m_pattern_quick_buttons;
MixedMixPreview *m_mix_preview = nullptr;
wxStaticText *m_breakdown_label = nullptr;
wxPanel *m_swatch = nullptr;
std::shared_ptr<std::vector<int>> m_selected_weight_state;
OnChangeFn m_on_change;
};
} } // namespace Slic3r::GUI
+272
View File
@@ -0,0 +1,272 @@
#include "MixedGradientSelector.hpp"
#include "GUI_App.hpp" // wxGetApp() / dark_mode()
#include "I18N.hpp" // _L()
#include "Widgets/Label.hpp" // Label::Body_10
#include "libslic3r/filament_mixer.h" // filament_mixer_lerp
#include <wx/dcbuffer.h>
#include <algorithm>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helper: copied verbatim from FullSpectrum Plater.cpp:2424
// ---------------------------------------------------------------------------
namespace {
wxColour blend_pair_filament_mixer(const wxColour &left, const wxColour &right, float t)
{
const wxColour safe_left = left.IsOk() ? left : wxColour("#26A69A");
const wxColour safe_right = right.IsOk() ? right : wxColour("#26A69A");
unsigned char out_r = static_cast<unsigned char>(safe_left.Red());
unsigned char out_g = static_cast<unsigned char>(safe_left.Green());
unsigned char out_b = static_cast<unsigned char>(safe_left.Blue());
::Slic3r::filament_mixer_lerp(static_cast<unsigned char>(safe_left.Red()),
static_cast<unsigned char>(safe_left.Green()),
static_cast<unsigned char>(safe_left.Blue()),
static_cast<unsigned char>(safe_right.Red()),
static_cast<unsigned char>(safe_right.Green()),
static_cast<unsigned char>(safe_right.Blue()),
std::clamp(t, 0.f, 1.f),
&out_r, &out_g, &out_b);
return wxColour(out_r, out_g, out_b);
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Constructor / destructor
// ---------------------------------------------------------------------------
MixedGradientSelector::MixedGradientSelector(wxWindow *parent,
const wxColour &left,
const wxColour &right,
int value_percent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
, m_left(left)
, m_right(right)
, m_value(std::clamp(value_percent, 0, 100))
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(wxSize(FromDIP(96), FromDIP(12)));
Bind(wxEVT_PAINT, &MixedGradientSelector::on_paint, this);
Bind(wxEVT_LEFT_DOWN, &MixedGradientSelector::on_left_down, this);
Bind(wxEVT_LEFT_UP, &MixedGradientSelector::on_left_up, this);
Bind(wxEVT_MOTION, &MixedGradientSelector::on_mouse_move, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &MixedGradientSelector::on_capture_lost, this);
}
MixedGradientSelector::~MixedGradientSelector()
{
if (HasCapture())
ReleaseMouse();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void MixedGradientSelector::set_colors(const wxColour &left, const wxColour &right)
{
m_left = left;
m_right = right;
m_multi_mode = false;
m_multi_colors.clear();
m_multi_weights.clear();
Refresh();
}
void MixedGradientSelector::set_multi_preview(const std::vector<wxColour> &corner_colors,
const std::vector<int> &weights)
{
m_multi_mode = corner_colors.size() >= 3;
m_multi_colors = corner_colors;
m_multi_weights = weights;
Refresh();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
wxRect MixedGradientSelector::gradient_rect() const
{
const int margin_x = FromDIP(2);
const int margin_y = FromDIP(1);
const wxSize sz = GetClientSize();
return wxRect(margin_x, margin_y,
std::max(1, sz.GetWidth() - margin_x * 2),
std::max(1, sz.GetHeight() - margin_y * 2));
}
int MixedGradientSelector::value_from_x(int x) const
{
const wxRect rect = gradient_rect();
const int min_x = rect.GetLeft();
const int max_x = rect.GetLeft() + rect.GetWidth();
const int clamp_x = std::clamp(x, min_x, max_x);
return ((clamp_x - min_x) * 100 + rect.GetWidth() / 2) / rect.GetWidth();
}
void MixedGradientSelector::update_from_x(int x, bool notify)
{
m_value = value_from_x(x);
Refresh();
if (notify) {
wxCommandEvent evt(wxEVT_SLIDER, GetId());
evt.SetInt(m_value);
evt.SetEventObject(this);
ProcessWindowEvent(evt);
}
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
void MixedGradientSelector::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const bool is_dark = wxGetApp().dark_mode();
const wxRect rect = gradient_rect();
if (m_multi_mode && m_multi_colors.size() >= 3) {
const wxPoint tl(rect.GetLeft(), rect.GetTop());
const wxPoint tr(rect.GetRight(), rect.GetTop());
const wxPoint br(rect.GetRight(), rect.GetBottom());
const wxPoint bl(rect.GetLeft(), rect.GetBottom());
const wxPoint cc(rect.GetLeft() + rect.GetWidth() / 2,
rect.GetTop() + rect.GetHeight() / 2);
auto draw_tri = [&dc](const wxColour &color,
const wxPoint &a,
const wxPoint &b,
const wxPoint &c) {
wxPoint pts[3] = { a, b, c };
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(color));
dc.DrawPolygon(3, pts);
};
if (m_multi_colors.size() >= 4) {
draw_tri(m_multi_colors[0], tl, tr, cc);
draw_tri(m_multi_colors[1], tr, br, cc);
draw_tri(m_multi_colors[2], br, bl, cc);
draw_tri(m_multi_colors[3], bl, tl, cc);
} else {
// 3-colour layout: first colour occupies one full side, two others on the opposite corners.
draw_tri(m_multi_colors[0], tl, bl, cc);
draw_tri(m_multi_colors[1], tl, tr, cc);
draw_tri(m_multi_colors[2], bl, br, cc);
}
if (m_multi_weights.size() == m_multi_colors.size()) {
dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(20, 20, 20));
dc.SetFont(Label::Body_10);
const int pad = FromDIP(2);
if (m_multi_colors.size() >= 4) {
dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]),
rect.GetLeft() + pad, rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]),
rect.GetRight() - FromDIP(28), rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]),
rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14));
dc.DrawText(wxString::Format("%d%%", m_multi_weights[3]),
rect.GetLeft() + pad, rect.GetBottom() - FromDIP(14));
} else {
dc.DrawText(wxString::Format("%d%%", m_multi_weights[0]),
rect.GetLeft() + pad,
rect.GetTop() + rect.GetHeight() / 2 - FromDIP(6));
dc.DrawText(wxString::Format("%d%%", m_multi_weights[1]),
rect.GetRight() - FromDIP(28), rect.GetTop() + pad);
dc.DrawText(wxString::Format("%d%%", m_multi_weights[2]),
rect.GetRight() - FromDIP(28), rect.GetBottom() - FromDIP(14));
}
}
} else {
const int w = rect.GetWidth();
const int h = rect.GetHeight();
wxImage img(w, h);
unsigned char *data = img.GetData();
if (data != nullptr) {
for (int x = 0; x < w; ++x) {
const float t = (w > 1) ? float(x) / float(w - 1) : 0.5f;
const wxColour col = blend_pair_filament_mixer(m_left, m_right, t);
const unsigned char r = static_cast<unsigned char>(col.Red());
const unsigned char g = static_cast<unsigned char>(col.Green());
const unsigned char b = static_cast<unsigned char>(col.Blue());
for (int y = 0; y < h; ++y) {
const int idx = (y * w + x) * 3;
data[idx + 0] = r;
data[idx + 1] = g;
data[idx + 2] = b;
}
}
dc.DrawBitmap(wxBitmap(img), rect.GetLeft(), rect.GetTop(), false);
} else {
dc.GradientFillLinear(rect, m_left, m_right, wxEAST);
}
}
dc.SetPen(wxPen(is_dark ? wxColour(100, 100, 106) : wxColour(170, 170, 170), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
if (m_multi_mode) {
dc.SetTextForeground(is_dark ? wxColour(236, 236, 236) : wxColour(30, 30, 30));
dc.SetFont(Label::Body_10);
const wxString hint = _L("Click to edit");
wxSize text_sz = dc.GetTextExtent(hint);
dc.DrawText(hint, rect.GetRight() - text_sz.GetWidth() - FromDIP(4), rect.GetTop() + FromDIP(2));
return;
}
int marker_x = rect.GetLeft() + (rect.GetWidth() * m_value + 50) / 100;
marker_x = std::clamp(marker_x, rect.GetLeft(), rect.GetRight());
dc.SetPen(wxPen(wxColour(255, 255, 255), 3));
dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom());
dc.SetPen(wxPen(wxColour(33, 33, 33), 1));
dc.DrawLine(marker_x, rect.GetTop(), marker_x, rect.GetBottom());
}
void MixedGradientSelector::on_left_down(wxMouseEvent &evt)
{
if (m_multi_mode)
return;
if (!HasCapture())
CaptureMouse();
m_dragging = true;
update_from_x(evt.GetX(), false);
}
void MixedGradientSelector::on_left_up(wxMouseEvent &evt)
{
if (m_multi_mode) {
wxCommandEvent click_evt(wxEVT_BUTTON, GetId());
click_evt.SetEventObject(this);
ProcessWindowEvent(click_evt);
return;
}
if (m_dragging)
update_from_x(evt.GetX(), true);
m_dragging = false;
if (HasCapture())
ReleaseMouse();
}
void MixedGradientSelector::on_mouse_move(wxMouseEvent &evt)
{
if (m_dragging && evt.LeftIsDown())
update_from_x(evt.GetX(), false);
}
void MixedGradientSelector::on_capture_lost(wxMouseCaptureLostEvent &)
{
m_dragging = false;
}
} } // namespace Slic3r::GUI
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <wx/panel.h>
#include <wx/colour.h>
#include <vector>
#include <functional>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// MixedGradientSelector
//
// A small horizontal panel that renders a two-colour gradient (or a
// multi-colour preview in "multi mode") and lets the user drag a marker
// to pick a blend percentage. In multi mode the panel renders coloured
// triangles showing corner weights and emits wxEVT_BUTTON on click so the
// owner can open MixedGradientWeightsDialog.
//
// Extracted from FullSpectrum Plater.cpp:4290-4505.
// ---------------------------------------------------------------------------
class MixedGradientSelector : public wxPanel
{
public:
MixedGradientSelector(wxWindow *parent,
const wxColour &left,
const wxColour &right,
int value_percent);
~MixedGradientSelector() override;
// Current blend value 0-100.
int value() const { return m_value; }
bool is_multi_mode() const { return m_multi_mode; }
// Switch to two-colour gradient mode.
void set_colors(const wxColour &left, const wxColour &right);
// Switch to multi-colour preview mode (>= 3 corner colours required).
void set_multi_preview(const std::vector<wxColour> &corner_colors,
const std::vector<int> &weights);
private:
wxRect gradient_rect() const;
int value_from_x(int x) const;
void update_from_x(int x, bool notify);
void on_paint(wxPaintEvent &evt);
void on_left_down(wxMouseEvent &evt);
void on_left_up(wxMouseEvent &evt);
void on_mouse_move(wxMouseEvent &evt);
void on_capture_lost(wxMouseCaptureLostEvent &evt);
wxColour m_left;
wxColour m_right;
bool m_multi_mode { false };
std::vector<wxColour> m_multi_colors;
std::vector<int> m_multi_weights;
int m_value { 50 };
bool m_dragging { false };
};
} } // namespace Slic3r::GUI
@@ -0,0 +1,150 @@
#include "MixedGradientWeightsDialog.hpp"
#include "MixedFilamentColorMapPanel.hpp"
#include "I18N.hpp" // _L()
#include "Widgets/Label.hpp" // Label::Body_12
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/panel.h>
#include <algorithm>
#include <cmath>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Anonymous-namespace helper: copied verbatim from FullSpectrum Plater.cpp:2558
// ---------------------------------------------------------------------------
namespace {
std::vector<int> normalize_color_match_weights(const std::vector<int> &weights, size_t count)
{
std::vector<int> out = weights;
if (out.size() != count)
out.assign(count, count > 0 ? int(100 / int(count)) : 0);
int sum = 0;
for (int &value : out) {
value = std::max(0, value);
sum += value;
}
if (sum <= 0 && count > 0) {
out.assign(count, 0);
out[0] = 100;
return out;
}
std::vector<double> remainders(count, 0.0);
int assigned = 0;
for (size_t idx = 0; idx < count; ++idx) {
const double exact = 100.0 * double(out[idx]) / double(sum);
out[idx] = int(std::floor(exact));
remainders[idx] = exact - double(out[idx]);
assigned += out[idx];
}
int missing = std::max(0, 100 - assigned);
while (missing > 0) {
size_t best_idx = 0;
double best_remainder = -1.0;
for (size_t idx = 0; idx < remainders.size(); ++idx) {
if (remainders[idx] > best_remainder) {
best_remainder = remainders[idx];
best_idx = idx;
}
}
++out[best_idx];
remainders[best_idx] = 0.0;
--missing;
}
return out;
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Constructor — verbatim from FullSpectrum Plater.cpp:4506-4583
// ---------------------------------------------------------------------------
MixedGradientWeightsDialog::MixedGradientWeightsDialog(
wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights)
: wxDialog(parent, wxID_ANY, _L("Gradient Mix Weights"),
wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
m_colors.reserve(filament_ids.size());
m_weights = normalize_color_match_weights(initial_weights, filament_ids.size());
for (const unsigned int filament_id : filament_ids) {
if (filament_id >= 1 && filament_id <= palette.size())
m_colors.emplace_back(palette[filament_id - 1]);
else
m_colors.emplace_back(wxColour("#26A69A"));
}
if (m_colors.empty())
m_colors.emplace_back(wxColour("#26A69A"));
auto *root = new wxBoxSizer(wxVERTICAL);
auto *hint = new wxStaticText(this, wxID_ANY,
_L("Pick a point in the gradient map to control multi-filament mix."));
root->Add(hint, 0, wxEXPAND | wxALL, FromDIP(10));
m_color_map = new MixedFilamentColorMapPanel(this, filament_ids, palette, initial_weights,
wxSize(FromDIP(240), FromDIP(240)));
root->Add(m_color_map, 1, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(10));
for (size_t i = 0; i < filament_ids.size(); ++i) {
auto *row = new wxBoxSizer(wxHORIZONTAL);
wxPanel *chip = new wxPanel(this, wxID_ANY, wxDefaultPosition,
wxSize(FromDIP(18), FromDIP(18)), wxBORDER_SIMPLE);
chip->SetBackgroundColour(m_colors[i]);
row->Add(chip, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
row->Add(new wxStaticText(this, wxID_ANY,
wxString::Format("F%d", int(filament_ids[i]))),
0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8));
auto *label = new wxStaticText(this, wxID_ANY,
wxString::Format("%d%%", m_weights[i]));
label->SetFont(Label::Body_12);
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
root->Add(row, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(8));
m_weight_labels.emplace_back(label);
}
root->Add(CreateSeparatedButtonSizer(wxOK | wxCANCEL), 0, wxEXPAND | wxALL, FromDIP(8));
SetSizerAndFit(root);
SetMinSize(wxSize(FromDIP(380),
std::max(GetSize().GetHeight(), FromDIP(460))));
update_weight_labels();
if (m_color_map) {
m_color_map->Bind(wxEVT_SLIDER, [this](wxCommandEvent &) {
m_weights = m_color_map ? m_color_map->normalized_weights() : m_weights;
update_weight_labels();
});
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
std::vector<int> MixedGradientWeightsDialog::normalized_weights() const
{
return m_color_map ? m_color_map->normalized_weights() : m_weights;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
void MixedGradientWeightsDialog::update_weight_labels()
{
for (size_t i = 0; i < m_weight_labels.size() && i < m_weights.size(); ++i) {
if (m_weight_labels[i])
m_weight_labels[i]->SetLabel(wxString::Format("%d%%", m_weights[i]));
}
Layout();
}
} } // namespace Slic3r::GUI
@@ -0,0 +1,45 @@
#pragma once
#include <wx/dialog.h>
#include <wx/colour.h>
#include <wx/stattext.h>
#include <vector>
namespace Slic3r { namespace GUI {
// Forward-declare Task-17 panel: defined in MixedFilamentColorMapPanel.hpp.
class MixedFilamentColorMapPanel;
// ---------------------------------------------------------------------------
// MixedGradientWeightsDialog
//
// A modal dialog that shows a MixedFilamentColorMapPanel and per-filament
// weight labels so the user can pick multi-filament blend weights for a
// gradient mix.
//
// Extracted from FullSpectrum Plater.cpp:4506-4583.
//
// NOTE (Task 17 dependency): the constructor body that instantiates
// MixedFilamentColorMapPanel is guarded with #if 0 until Task 17 lands.
// See MixedGradientWeightsDialog.cpp for details.
// ---------------------------------------------------------------------------
class MixedGradientWeightsDialog : public wxDialog
{
public:
MixedGradientWeightsDialog(wxWindow *parent,
const std::vector<unsigned int> &filament_ids,
const std::vector<wxColour> &palette,
const std::vector<int> &initial_weights);
// Returns the normalised per-filament weight vector chosen by the user.
std::vector<int> normalized_weights() const;
private:
void update_weight_labels();
MixedFilamentColorMapPanel *m_color_map { nullptr };
std::vector<wxColour> m_colors;
std::vector<int> m_weights;
std::vector<wxStaticText *> m_weight_labels;
};
} } // namespace Slic3r::GUI
+181
View File
@@ -0,0 +1,181 @@
#include "MixedMixPreview.hpp"
#include "GUI_App.hpp" // wxGetApp() / dark_mode()
#include <wx/dcbuffer.h> // wxAutoBufferedPaintDC
#include <algorithm>
#include <cmath>
namespace Slic3r { namespace GUI {
// ---------------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------------
MixedMixPreview::MixedMixPreview(wxWindow *parent)
: wxPanel(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE)
{
SetBackgroundStyle(wxBG_STYLE_PAINT);
SetMinSize(wxSize(FromDIP(120), FromDIP(20)));
Bind(wxEVT_PAINT, &MixedMixPreview::on_paint, this);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void MixedMixPreview::set_data(const std::vector<wxColour> &palette,
const std::vector<unsigned int> &sequence,
bool same_layer_mode,
const std::vector<double> &surface_offsets_mm,
const wxColour &fallback,
const wxString &left_overlay,
const wxString &right_overlay)
{
m_palette = palette;
m_sequence = sequence;
m_same_layer = same_layer_mode;
m_surface_offsets_mm = surface_offsets_mm;
m_fallback = fallback;
m_left_overlay = left_overlay;
m_right_overlay = right_overlay;
Refresh();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
wxRect MixedMixPreview::preview_rect() const
{
const int margin_x = FromDIP(1);
const int margin_y = FromDIP(1);
const wxSize sz = GetClientSize();
return wxRect(margin_x, margin_y,
std::max(1, sz.GetWidth() - margin_x * 2),
std::max(1, sz.GetHeight() - margin_y * 2));
}
wxColour MixedMixPreview::color_for_extruder(unsigned int extruder_id) const
{
if (extruder_id >= 1 && extruder_id <= m_palette.size())
return m_palette[extruder_id - 1];
return m_fallback;
}
double MixedMixPreview::max_active_surface_offset_mm() const
{
double max_offset = 0.0;
for (double offset_mm : m_surface_offsets_mm)
max_offset = std::max(max_offset, std::abs(offset_mm));
return std::max(0.001, max_offset);
}
int MixedMixPreview::slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const
{
if (extruder_id == 0 || extruder_id >= m_surface_offsets_mm.size() || slot_extent <= 2)
return 0;
const double offset_mm = m_surface_offsets_mm[extruder_id];
if (std::abs(offset_mm) <= EPSILON)
return 0;
const double normalized = std::clamp(std::abs(offset_mm) / max_active_surface_offset_mm(), 0.0, 1.0);
const int inset = int(std::round(normalized * slot_extent * 0.45))
* (offset_mm < 0.0 ? -1 : 1);
return std::clamp(inset,
-std::max(0, slot_extent / 2),
std::max(0, slot_extent / 2));
}
// ---------------------------------------------------------------------------
// Paint handler
// ---------------------------------------------------------------------------
void MixedMixPreview::on_paint(wxPaintEvent &)
{
wxAutoBufferedPaintDC dc(this);
dc.SetBackground(wxBrush(GetBackgroundColour()));
dc.Clear();
const wxRect rect = preview_rect();
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(m_fallback));
dc.DrawRectangle(rect);
if (!m_sequence.empty()) {
if (m_same_layer) {
// Same-layer preview: full-height stripe lines.
const int stripes = 24;
const int stripe_w = std::max(1, rect.GetWidth() / stripes);
const size_t seq_len = m_sequence.size();
for (int s = 0; s < stripes; ++s) {
const size_t idx = size_t(s % int(seq_len));
const unsigned int extruder_id = m_sequence[idx];
dc.SetBrush(wxBrush(color_for_extruder(extruder_id)));
const int x = rect.GetLeft() + s * stripe_w;
const int w = (s == stripes - 1) ? (rect.GetRight() - x + 1) : stripe_w;
const int inset = slot_inset_for_extruder(extruder_id, w);
wxRect draw_rect(x + inset / 2, rect.GetTop(),
std::max(1, w - inset), rect.GetHeight());
draw_rect.Intersect(rect);
if (draw_rect.GetWidth() > 0)
dc.DrawRectangle(draw_rect);
}
} else {
const int bars = 24;
const int bar_w = std::max(1, rect.GetWidth() / bars);
for (int i = 0; i < bars; ++i) {
size_t idx = 0;
if (m_sequence.size() > size_t(bars))
idx = (size_t(i) * m_sequence.size()) / size_t(bars);
else
idx = size_t(i) % m_sequence.size();
const unsigned int extruder_id = m_sequence[idx];
dc.SetBrush(wxBrush(color_for_extruder(extruder_id)));
const int x = rect.GetLeft() + i * bar_w;
const int w = (i == bars - 1) ? (rect.GetRight() - x + 1) : bar_w;
const int inset = slot_inset_for_extruder(extruder_id, w);
wxRect draw_rect(x + inset / 2, rect.GetTop(),
std::max(1, w - inset), rect.GetHeight());
draw_rect.Intersect(rect);
if (draw_rect.GetWidth() > 0)
dc.DrawRectangle(draw_rect);
}
}
}
auto draw_outlined_text = [this, &dc](const wxString &text, int x, int y) {
if (text.empty())
return;
dc.SetTextForeground(wxColour(255, 255, 255));
const int outline_radius = std::max(2, FromDIP(2));
for (int ox = -outline_radius; ox <= outline_radius; ++ox) {
for (int oy = -outline_radius; oy <= outline_radius; ++oy) {
if (ox == 0 && oy == 0)
continue;
dc.DrawText(text, x + ox, y + oy);
}
}
dc.SetTextForeground(wxColour(22, 22, 22));
dc.DrawText(text, x, y);
};
wxCoord left_w = 0, left_h = 0;
wxCoord right_w = 0, right_h = 0;
dc.GetTextExtent(m_left_overlay, &left_w, &left_h);
dc.GetTextExtent(m_right_overlay, &right_w, &right_h);
const int text_y = rect.GetTop()
+ std::max(0, (rect.GetHeight() - int(std::max(left_h, right_h))) / 2);
const int pad = FromDIP(6);
if (!m_left_overlay.empty())
draw_outlined_text(m_left_overlay, rect.GetLeft() + pad, text_y);
if (!m_right_overlay.empty())
draw_outlined_text(m_right_overlay, rect.GetRight() - pad - int(right_w), text_y);
const bool is_dark = wxGetApp().dark_mode();
dc.SetPen(wxPen(is_dark ? wxColour(110, 110, 110) : wxColour(170, 170, 170), 1));
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRectangle(rect);
}
} } // namespace Slic3r::GUI
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <wx/panel.h>
#include <wx/colour.h>
#include <vector>
#include <string>
namespace Slic3r { namespace GUI {
// Preview strip that shows the layer-by-layer or same-layer colour sequence
// produced by a mixed filament definition. All logic is self-contained; the
// owning panel calls set_data() whenever the underlying MixedFilament changes.
class MixedMixPreview : public wxPanel
{
public:
explicit MixedMixPreview(wxWindow *parent);
void set_data(const std::vector<wxColour> &palette,
const std::vector<unsigned int> &sequence,
bool same_layer_mode,
const std::vector<double> &surface_offsets_mm,
const wxColour &fallback,
const wxString &left_overlay,
const wxString &right_overlay);
private:
wxRect preview_rect() const;
wxColour color_for_extruder(unsigned int extruder_id) const;
double max_active_surface_offset_mm() const;
int slot_inset_for_extruder(unsigned int extruder_id, int slot_extent) const;
void on_paint(wxPaintEvent &evt);
std::vector<wxColour> m_palette;
std::vector<unsigned int> m_sequence;
std::vector<double> m_surface_offsets_mm;
bool m_same_layer { false };
wxColour m_fallback { wxColour(38, 166, 154) };
wxString m_left_overlay;
wxString m_right_overlay;
};
} } // namespace Slic3r::GUI
+2
View File
@@ -166,6 +166,8 @@ enum class NotificationType
OrcaSharedProfilesAvailable, OrcaSharedProfilesAvailable,
OrcaCloudAPIError, OrcaCloudAPIError,
OrcaSyncConflict, OrcaSyncConflict,
BBLMixedFilamentBroken,
BBLSingleExtruderMixedFilamentRisk,
NotificationTypeCount NotificationTypeCount
}; };
+19 -3
View File
@@ -423,15 +423,31 @@ void ObjectDataViewModelNode::UpdateExtruderAndColorIcon(wxString extruder /*= "
} }
} }
const size_t extruder_id_1based = extruder_idx;
if (extruder_idx > 0) --extruder_idx; if (extruder_idx > 0) --extruder_idx;
// Create the bitmap with color bars. // Create the bitmap with color bars.
std::vector<wxBitmap*> bmps = get_extruder_color_icons(false);// use wide icons std::vector<wxBitmap*> bmps = get_extruder_color_icons(false);// use wide icons
if (bmps.empty()) { if (!bmps.empty() && extruder_idx < bmps.size()) {
m_extruder_bmp = wxNullBitmap; m_extruder_bmp = *bmps[extruder_idx];
return; 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();
} }
// ***************************************************************************** // *****************************************************************************
+111 -3
View File
@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <regex> #include <regex>
#include <sstream>
#include <future> #include <future>
#include <glad/gl.h> #include <glad/gl.h>
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
@@ -23,6 +24,7 @@
#include "libslic3r/Tesselate.hpp" #include "libslic3r/Tesselate.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp" #include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "libslic3r/MixedFilament.hpp"
#include "I18N.hpp" #include "I18N.hpp"
#include "GUI_App.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()); std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(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)); 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> PartPlate::get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const
{ {
std::vector<int> plate_extruders; 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 // if 3mf file
int glb_support_intf_extr = full_config.opt_int("support_interface_filament"); 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())) if ((obj_id >= 0) && (obj_id < m_model->objects.size()))
{ {
ModelObject* object = m_model->objects[obj_id]; 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]; 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) if (!instance->printable)
continue; 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()); std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(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)); 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 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()); std::sort(plate_extruders.begin(), plate_extruders.end());
auto it_end = std::unique(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)); 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*/ /* -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; 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]; return the_map->values[zero_base_logical_idx];
} }
+12
View File
@@ -216,6 +216,18 @@ OtherLayersSeqPanel::OtherLayersSeqPanel(wxWindow* parent)
Layout(); Layout();
top_sizer->Fit(this); 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) { m_other_layer_print_seq_choice->Bind(wxEVT_COMBOBOX, [this, buttons_sizer](auto& e) {
if (e.GetSelection() == 0) { if (e.GetSelection() == 0) {
+1284 -29
View File
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -182,6 +182,14 @@ public:
void on_filament_count_change(size_t num_filaments); void on_filament_count_change(size_t num_filaments);
void on_filaments_delete(size_t filament_id); 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 add_filament();
void delete_filament(size_t filament_id = size_t(-1), int replace_filament_id = -1); // 0 base, -1 means default 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 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_change(size_t filament_idx);
void on_filament_count_change(size_t extruders_count); 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(); std::vector<Slic3r::ColorRGBA> get_extruders_colors();
// BBS // BBS
void on_bed_type_change(BedType bed_type); void on_bed_type_change(BedType bed_type);
@@ -568,7 +587,7 @@ public:
void force_print_bed_update(); void force_print_bed_update();
// On activating the parent window. // On activating the parent window.
void on_activate(); 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_colors_render_info() const;
std::vector<std::string> get_filament_color_render_type() 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; std::vector<std::string> get_colors_for_color_print(const GCodeProcessorResult* const result = nullptr) const;
+17
View File
@@ -7,6 +7,7 @@
#include "I18N.hpp" #include "I18N.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp" #include "libslic3r/Format/DRC.hpp"
#include "libslic3r/MixedFilament.hpp"
#include <wx/language.h> #include <wx/language.h>
#include "OG_CustomCtrl.hpp" #include "OG_CustomCtrl.hpp"
#include "wx/graphics.h" #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 (param == "enable_high_low_temp_mixed_printing") {
if (checkbox->GetValue()) { if (checkbox->GetValue()) {
const wxString warning_title = _L("Bed Temperature Difference Warning"); 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); 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); 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"); auto item_auto_arrange = create_item_checkbox(_L("Auto arrange plate after cloning"), "", "auto_arrange");
g_sizer->Add(item_auto_arrange); g_sizer->Add(item_auto_arrange);
+99
View File
@@ -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 // reload scene to update timelapse wipe tower
if (opt_key == "timelapse_type") { if (opt_key == "timelapse_type") {
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value; 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; 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(); update();
if(m_active_page) if(m_active_page)
m_active_page->update_visibility(m_mode, true); m_active_page->update_visibility(m_mode, true);
m_page_view->GetParent()->Layout(); 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() { 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"); 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("layer_height","quality_settings_layer_height");
optgroup->append_single_option_line("initial_layer_print_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 = page->new_optgroup(L("Line width"), L"param_line_width");
optgroup->append_single_option_line("line_width","quality_settings_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("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("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); 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("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("sparse_infill_rotate_template", "strength_settings_infill_rotation_template_metalanguage");
optgroup->append_single_option_line("skin_infill_density", "strength_settings_patterns#locked-zag"); 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_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("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("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 = 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"); 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("timelapse_type", "others_settings_special_mode#timelapse");
optgroup->append_single_option_line("enable_wrapping_detection"); 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 = 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", "others_settings_fuzzy_skin");
optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-mode"); optgroup->append_single_option_line("fuzzy_skin_mode", "others_settings_fuzzy_skin#fuzzy-skin-mode");
+108 -19
View File
@@ -199,24 +199,71 @@ std::string RammingPanel::get_parameters()
static const float g_min_flush_multiplier = 0.f; static const float g_min_flush_multiplier = 0.f;
static const float g_max_flush_multiplier = 3.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() bool is_flush_config_modified()
{ {
const auto &project_config = wxGetApp().preset_bundle->project_config; 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_matrix = (project_config.option<ConfigOptionFloats>("flush_volumes_matrix"))->values;
const std::vector<double> &config_multiplier = (project_config.option<ConfigOptionFloats>("flush_multiplier"))->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; 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) { if (config_multiplier[i] != 1) {
has_modify = true; has_modify = true;
break; 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); std::vector<std::vector<double>> default_matrix = WipingDialog::CalcFlushingVolumes(i);
int len = default_matrix.size(); // CalcFlushingVolumes also spans total×total; take physical sub-matrix.
for (int m = 0; m < len; m++) { int def_total = (int)default_matrix.size();
for (int n = 0; n < len; n++) { for (int m = 0; m < (int)num_phys; m++) {
int idx = i * len * len + m * len + n; for (int n = 0; n < (int)num_phys; n++) {
if (config_matrix[idx] != default_matrix[m][n] * config_multiplier[i]) { 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; has_modify = true;
break; break;
} }
@@ -265,24 +312,46 @@ wxString WipingDialog::BuildTableObjStr()
auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values; auto raw_matrix_data = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
auto nozzle_flush_dataset = full_config.option<ConfigOptionIntsNullable>("nozzle_flush_dataset")->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) { 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); 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) { 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; 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; json obj;
obj["flush_multiplier"] = flush_multiplier; obj["flush_multiplier"] = flush_multiplier;
obj["extruder_num"] = nozzle_num; obj["extruder_num"] = nozzle_num;
obj["filament_colors"] = filament_colors; obj["filament_colors"] = phys_colors;
obj["flush_volume_matrixs"] = json::array(); obj["flush_volume_matrixs"] = json::array();
obj["min_flush_volumes"] = json::array(); obj["min_flush_volumes"] = json::array();
obj["max_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) { 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); 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]); GenericFlushPredictor pd(nozzle_flush_dataset[idx]);
int min_flush_from_flush_data = pd.get_min_flush_volume(); 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)); 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") { else if (j["msg"].get<std::string>() == "storeData") {
int extruder_num = j["number_of_extruders"].get<int>(); 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) { 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>>(); 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 {// limit all matrix value before write to gcode, the limitation is depends on the multipliers
size_t cols_temp_matrix = 0; size_t cols_temp_matrix = 0;
if (!store_matrixs.empty()) { cols_temp_matrix = store_matrixs[0].size(); } if (!phys_matrixs.empty()) { cols_temp_matrix = phys_matrixs[0].size(); }
if (store_multipliers.size() == store_matrixs.size() && cols_temp_matrix>0) // nuzzles==nuzzles if (store_multipliers.size() == phys_matrixs.size() && cols_temp_matrix>0) // nuzzles==nuzzles
{ {
for (size_t idx = 0; idx < store_multipliers.size(); ++idx) { 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]; 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(), std::transform(phys_matrixs[idx].begin(), phys_matrixs[idx].end(),
store_matrixs[idx].begin(), phys_matrixs[idx].begin(),
[m_max_flush_volume_t, m_store_multipliers](double inputx) { [m_max_flush_volume_t, m_store_multipliers](double inputx) {
return std::clamp(inputx, 0.0, m_max_flush_volume_t / m_store_multipliers); 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); this->StoreFlushData(extruder_num, store_matrixs, store_multipliers);
m_submit_flag = true; m_submit_flag = true;
this->Close(); this->Close();
+20 -2
View File
@@ -783,6 +783,24 @@ void apply_extruder_selector(Slic3r::GUI::BitmapComboBox** ctrl,
bool use_thin_icon/* = false*/) bool use_thin_icon/* = false*/)
{ {
std::vector<wxBitmap*> icons = get_extruder_color_icons(use_thin_icon); 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) { if (!*ctrl) {
*ctrl = new Slic3r::GUI::BitmapComboBox(parent, wxID_ANY, wxEmptyString, pos, size, 0, nullptr, wxCB_READONLY); *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) { for (wxBitmap* bmp : icons) {
if (i == 0) { if (i == 0) {
if (!first_item.empty()) if (!first_item.empty())
(*ctrl)->Append(_(first_item), *bmp); (*ctrl)->Append(_(first_item), bmp ? *bmp : wxNullBitmap);
++i; ++i;
} }
(*ctrl)->Append(use_full_item_name (*ctrl)->Append(use_full_item_name
? Slic3r::GUI::from_u8((boost::format("%1% %2%") % str % i).str()) ? Slic3r::GUI::from_u8((boost::format("%1% %2%") % str % i).str())
: wxString::Format("%d", i), *bmp); : wxString::Format("%d", i), bmp ? *bmp : wxNullBitmap);
++i; ++i;
} }
(*ctrl)->SetSelection(0); (*ctrl)->SetSelection(0);
+1
View File
@@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests
test_flow.cpp test_flow.cpp
test_gcode.cpp test_gcode.cpp
test_gcodewriter.cpp test_gcodewriter.cpp
test_mixed_filament_e2e.cpp
test_model.cpp test_model.cpp
test_print.cpp test_print.cpp
test_printgcode.cpp test_printgcode.cpp
+218
View File
@@ -0,0 +1,218 @@
// Tests for mixed-filament project-config round-trip persistence.
//
// The full-3MF (store_bbs_3mf / load_bbs_3mf) round-trip requires a heavy
// PlateDataPtrs + Model + PresetBundle setup that is not yet scaffolded in
// fff_print tests. We therefore exercise the *persistence layer* directly:
//
// MixedFilamentManager::serialize_custom_entries()
// → stored in project_config["mixed_filament_definitions"]
// → PresetBundle::sync_mixed_filaments_to_config() (mirrors store path)
// → PresetBundle::sync_mixed_filaments_from_config() (mirrors load path)
//
// This is the code path that bbs_3mf.cpp invokes when writing/reading the
// project config block, so a regression here would break 3MF persistence.
//
// TODO: full-pipeline E2E slice test (T0/T1 in G-code, dithering_local_z_mode
// sublayer assertions) requires the full Print::process() scaffolding;
// defer to a follow-up once a minimal PrintObject fixture exists.
#include <catch2/catch_all.hpp>
#include "libslic3r/MixedFilament.hpp"
#include "libslic3r/PresetBundle.hpp"
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
using namespace Slic3r;
// ---------------------------------------------------------------------------
// Helper: build a two-filament PresetBundle with colours set.
// ---------------------------------------------------------------------------
namespace {
static PresetBundle make_bundle_2(const std::string &col_a = "#FF0000",
const std::string &col_b = "#0000FF")
{
PresetBundle bundle;
bundle.filament_presets = {"Default Filament", "Default Filament"};
bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {col_a, col_b};
return bundle;
}
} // anonymous namespace
// ---------------------------------------------------------------------------
// Test 1 — plain auto-generated round-trip
// Build bundle → sync to config string → reload in fresh bundle → compare
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: auto-generated entries survive serialize/load cycle",
"[MixedFilamentRoundTrip]")
{
PresetBundle origin = make_bundle_2();
origin.sync_mixed_filaments_from_config();
const auto &orig_mixed = origin.mixed_filaments.mixed_filaments();
REQUIRE(!orig_mixed.empty());
// Capture the stable_id and component IDs of the first enabled entry.
const MixedFilament *first = nullptr;
for (const auto &mf : orig_mixed) {
if (mf.enabled && !mf.deleted) { first = &mf; break; }
}
REQUIRE(first != nullptr);
const uint64_t orig_stable_id = first->stable_id;
const unsigned int orig_component_a = first->component_a;
const unsigned int orig_component_b = first->component_b;
// Sync to config (mirrors what store_bbs_3mf does).
origin.sync_mixed_filaments_to_config();
const std::string serialized = origin.project_config.opt_string("mixed_filament_definitions");
// Auto-generated entries are NOT stored in the custom definitions string —
// they are rebuilt by auto_generate() on load. The string will be empty
// unless a custom entry was added. We just verify the round-trip doesn't
// drop auto-generated rows on re-sync.
// Reload into a fresh bundle with the same colours.
PresetBundle loaded = make_bundle_2();
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = serialized;
loaded.sync_mixed_filaments_from_config();
const auto &load_mixed = loaded.mixed_filaments.mixed_filaments();
REQUIRE(load_mixed.size() == orig_mixed.size());
// Find the matching entry by component pair.
const MixedFilament *reloaded = nullptr;
for (const auto &mf : load_mixed) {
if (mf.component_a == orig_component_a && mf.component_b == orig_component_b) {
reloaded = &mf;
break;
}
}
REQUIRE(reloaded != nullptr);
CHECK(reloaded->stable_id == orig_stable_id);
}
// ---------------------------------------------------------------------------
// Test 2 — custom entry round-trip
// 2 physical + 1 custom mixed entry: ratio_a, ratio_b, mix_b_percent, stable_id
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: custom entry count, components, ratio, stable_id",
"[MixedFilamentRoundTrip]")
{
const std::vector<std::string> colors = {"#FF0000", "#00FF00"};
MixedFilamentManager mgr;
// Add two custom entries to exercise the multi-entry path.
mgr.add_custom_filament(1, 2, 25, colors);
mgr.add_custom_filament(1, 2, 75, colors);
const auto &entries = mgr.mixed_filaments();
REQUIRE(entries.size() == 2);
const uint64_t stable_id_0 = entries[0].stable_id;
const uint64_t stable_id_1 = entries[1].stable_id;
CHECK(stable_id_0 != stable_id_1);
// Serialize and reload.
const std::string serialized = mgr.serialize_custom_entries();
REQUIRE(!serialized.empty());
MixedFilamentManager loaded;
loaded.load_custom_entries(serialized, colors);
const auto &reloaded = loaded.mixed_filaments();
REQUIRE(reloaded.size() == 2);
// Order must be preserved.
CHECK(reloaded[0].component_a == 1);
CHECK(reloaded[0].component_b == 2);
CHECK(reloaded[0].mix_b_percent == 25);
CHECK(reloaded[0].stable_id == stable_id_0);
CHECK(reloaded[1].component_a == 1);
CHECK(reloaded[1].component_b == 2);
CHECK(reloaded[1].mix_b_percent == 75);
CHECK(reloaded[1].stable_id == stable_id_1);
}
// ---------------------------------------------------------------------------
// Test 3 — PresetBundle project_config string path (mirrors 3MF store+load)
// Verify that sync_mixed_filaments_to_config + sync_mixed_filaments_from_config
// preserves a custom entry end-to-end through the project_config string.
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: PresetBundle project_config string path",
"[MixedFilamentRoundTrip]")
{
PresetBundle origin = make_bundle_2("#FFFF00", "#FF00FF");
origin.sync_mixed_filaments_from_config();
// Add a custom entry on top of auto-generated ones.
const auto &colors = origin.project_config.option<ConfigOptionStrings>("filament_colour")->values;
origin.mixed_filaments.add_custom_filament(1, 2, 40, colors);
const size_t num_entries_before = origin.mixed_filaments.mixed_filaments().size();
// Count custom entries in origin.
size_t custom_count_before = 0;
uint64_t custom_stable_id = 0;
for (const auto &mf : origin.mixed_filaments.mixed_filaments()) {
if (mf.custom && mf.enabled && !mf.deleted) {
++custom_count_before;
custom_stable_id = mf.stable_id;
}
}
REQUIRE(custom_count_before == 1);
// Sync to config string — mirrors bbs_3mf store path.
origin.sync_mixed_filaments_to_config();
const std::string defs = origin.project_config.opt_string("mixed_filament_definitions");
REQUIRE(!defs.empty());
// Reload — mirrors bbs_3mf load path.
PresetBundle loaded = make_bundle_2("#FFFF00", "#FF00FF");
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = defs;
loaded.sync_mixed_filaments_from_config();
const auto &reloaded = loaded.mixed_filaments.mixed_filaments();
REQUIRE(reloaded.size() == num_entries_before);
// Custom entry must survive with its stable_id intact.
size_t custom_count_after = 0;
uint64_t reloaded_stable_id = 0;
for (const auto &mf : reloaded) {
if (mf.custom && mf.enabled && !mf.deleted) {
++custom_count_after;
reloaded_stable_id = mf.stable_id;
}
}
CHECK(custom_count_after == custom_count_before);
CHECK(reloaded_stable_id == custom_stable_id);
}
// ---------------------------------------------------------------------------
// Test 4 — total_filaments counts physical + mixed correctly after round-trip
// ---------------------------------------------------------------------------
TEST_CASE("Mixed filament 3MF round-trip: total_filaments count is stable after reload",
"[MixedFilamentRoundTrip]")
{
PresetBundle origin = make_bundle_2();
origin.sync_mixed_filaments_from_config();
const size_t num_physical = origin.filament_presets.size();
const size_t total_before = origin.mixed_filaments.total_filaments(num_physical);
// For 2 physical filaments C(2,2)=1 virtual → total must be 3.
REQUIRE(total_before == 3u);
origin.sync_mixed_filaments_to_config();
const std::string defs = origin.project_config.opt_string("mixed_filament_definitions");
PresetBundle loaded = make_bundle_2();
loaded.project_config.option<ConfigOptionString>("mixed_filament_definitions")->value = defs;
loaded.sync_mixed_filaments_from_config();
const size_t total_after = loaded.mixed_filaments.total_filaments(loaded.filament_presets.size());
CHECK(total_after == total_before);
}
+2
View File
@@ -16,6 +16,8 @@ add_executable(${_TEST_NAME}_tests
test_polygon.cpp test_polygon.cpp
test_mutable_polygon.cpp test_mutable_polygon.cpp
test_mutable_priority_queue.cpp test_mutable_priority_queue.cpp
test_mixed_filament.cpp
test_review_fixes.cpp
test_stl.cpp test_stl.cpp
test_meshboolean.cpp test_meshboolean.cpp
test_marchingsquares.cpp test_marchingsquares.cpp
File diff suppressed because it is too large Load Diff
+189
View File
@@ -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);
}