Skip to content

feat: mixed-filament phase 2 — batch color match with manual/recommended modes - #702

Merged
LiuLikeQian merged 72 commits into
mainfrom
merge_mix_filament_phase2
Aug 13, 2026
Merged

feat: mixed-filament phase 2 — batch color match with manual/recommended modes#702
LiuLikeQian merged 72 commits into
mainfrom
merge_mix_filament_phase2

Conversation

@zhangzhend0ng

Copy link
Copy Markdown
Collaborator

Summary

Phase 2 of the mixed-filament feature. Adds a batch color-match workflow that
extracts model colors, matches them against the Full Spectrum / physical
filament palette in CIELAB space, and generates mixed-filament recipes
(mix ratio or manual pattern) automatically, while keeping the Manual mode
fully editable.

What's new

Batch color match dialog (MixedFilamentBatchDialog)

  • New dialog with two match modes: Recommended (auto) and Manual.
  • Live dual previews (original vs. matched), plate navigation, named
    thumbnail viewpoints, and a mapping card showing model color → filament.
  • Footer with pinned progress bar + Stop for cancelling background matching.
  • Background matching runs on a worker thread; UI state is guarded for
    thread-safety (cancellation-aware).

Matching pipeline (MixedColorMatchHelpers, MixedFilament.cpp)

  • CIELAB-based color extraction and matching with a precomputed blend LUT
    (BlendLUT) and two-pass recipe generation (pure pairs first, then blends).
  • Recipe deduplication, per-component ratio cap (>70% advisory in manual mode,
    hard floor removed to allow legitimate near-pure recipes), and cancel-aware
    rematch.
  • Redundant filament computation: after a match, unused physical/mixed
    filaments are marked for cleanup with painting-remap (pure,
    unit-testable build_mixed_deletion_painting_remap).

Integration & fixes

  • Plater + GLCanvas3D: painting remap after mixed-row delete, batch-apply to
    parent objects, modifier volumes, and layer-object extruders; painting gizmo
    refresh on color change; object-list filament column updates after match.
  • PresetBundle: sync config extruder keys on physical-count expansion, reset
    keys to 0 instead of erasing, skip write-back when Full Spectrum is not
    selectable, gate Recommended mode on actual preset existence for the current
    nozzle diameter (no hard-coded 0.4).
  • Subdivide warnings retitled to "Configuration Conflict"; corrected default
    extruder handling in delete/batch-match/icon paths.
  • UI polish: rounded-corner previews via DC clipping (removes SetWindowRgn
    artifacts), opaque preview panel background, dual-color swatches, stable
    combo icons, collapsed footer progress to pin idle height.

Resources & localization

  • New SVG icons (mixed_filament_mapping_right_arrow*,
    mixed_filament_preview_placeholder, color_delta_e_label*, …).
  • 0.10 Color Mixing process preset reworded; Snapmaker.json updated.
  • zh_CN translation updated (+90).

Tests

  • tests/libslic3r/test_mixed_filament.cpp: +2329 lines covering core
    algorithms, deletion painting remap (single/multiple/all/empty/unsorted),
    batch-match recipe generation, redundant-filament computation, and physical
    token bounds guard.
  • tests/libslic3r/test_mixed_filament_color_golden.cpp: golden color-blend
    checks.

Verification

  • Build: Windows (VS2022) and Linux configured; unit suites pass.
  • Manual: batch match on multi-color models across plates, manual recipe
    editing, row delete → painting remap, nozzle-diameter preset switching.

zhangzhend0ng and others added 30 commits June 29, 2026 16:39
- MixedFilamentBatchEntry (wx-free, libslic3r layer)
- add_batch_custom_filaments declaration
- ModelColorEntry, ColorMappingEntry, BatchMatchResult (GUI layer)
- extract_model_colors implementation (ModelVolume::get_extruders + filament_colour)
- Function declarations for batch_match_model_colors, deduplicate, assign IDs, populate
- Color parsing dropped most model colors: try_parse_color_match_hex
  required exactly 7 chars, so alpha-bearing palette entries
  (#RRGGBBAA, as Bambu 3MF stores filament_colour) were rejected.
  Only 4 of 17 model colors survived and batch match produced 4
  mappings. normalize_color_match_hex now strips alpha
  (#RRGGBBAA -> #RRGGBB), matching Bambu parse_color / STUDIO-17977.
- Crash on repeated re-match after MANUAL -> RECOMMENDED:
  update_recommended_card deleted a wxBitmap returned by
  get_extruder_color_icon, but that pointer is owned by its static
  BitmapCache. The delete corrupted the cache and double-freed on the
  next match. Removed the delete; SetBitmap already copies, as all
  other callers do.
- Ghost pixels between legend rows on re-match: update_mapping_legend
  used deferred Destroy(), so removed rows kept owning their pixel
  regions and the parent never erased the gaps. Use Clear(true)
  (immediate delete) instead, like MixedFilamentDialog::rebuild_legend.
- Preserve the previous match result when switching mode or changing
  manual filament selection; only Start/Re-match clears it
  (new reset_match_preview() shared by the clear points).
Mixed-filament phase 2: integrate the prusa-fdm-mixer colour model as a
toggleable A/B demo, and fix several Mixed Color Match dialog defects.

prusa-fdm-mixer demo:
- Add ENABLE_PRUSA_FDM_MIXER_DEMO in Technologies.hpp (default 1). 0 keeps
  the legacy polynomial prediction byte-identical; 1 routes blend prediction
  through the calibrated prusa model -> CIELab for ΔE.
- Guard all call sites (MixedFilament, MixedColorMatchHelpers,
  MixedFilamentColorMapPanel, MixedGradientSelector, MixedFilamentDialog)
  with #if/#else.
- Add prusa_fdm_mixer.{cpp,hpp} + self-guarded wrap header; wire into
  CMake. Self-contained and removable.

batch match fixes:
- Color parsing dropped most palette colors: normalize_color_match_hex
  rejected alpha-bearing #RRGGBBAA (Bambu 3MF format) -> only 4 of 17
  matched. Strip alpha (#RRGGBBAA -> #RRGGBB).
- Crash on repeated re-match (MANUAL -> RECOMMENDED): removed delete of a
  wxBitmap owned by get_extruder_color_icon's static BitmapCache.
- Legend ghost pixels: update_mapping_legend uses Clear(true) instead of
  deferred Destroy().
- Preserve previous match result across mode/selection change; only
  Start/Re-match clears it (reset_match_preview).
- Recommended palette -> #08ABFB/#D93B90/#F9ED3D/#9199A4; keep input order
  in the result (stable for >4 candidates).
…move prusa demo

1. Batch match confirm flow: in-place rewrite + redundant cleanup
  - MixedFilamentManager::add_batch_custom_filaments gains an optional
    out_assigned_ids out-param so the caller can learn each entry's
    assigned virtual id before cleanup.  Every rejection path (n<2, cap
    reached, a==b, references out of range) pushes 0u, keeping the output
    strictly 1:1 with entries.
  - New pure helper compute_redundant_filaments: given the keep sets it
    decides which physical/mixed filaments are redundant (physical ids
    emitted descending for direct deletion; a survivor floor guarantees at
    least one physical is kept).  Cascade detection reuses remove_physical_
    filament's authoritative tokenizer to avoid drift.
  - New Sidebar::cleanup_unused_filaments_after_batch_match: capture each
    redundant mixed row's stable_id -> delete physical filaments (triggers
    remap) -> mark non-cascade redundant mixed rows by stable_id.
  - The confirm flow rewrites EXISTING custom mixed rows in place (new
    in_place_edited flag), preserving their virtual id and painting so
    dropping a component physical does not strand painting.
  - R3 guard: before cleanup, scan every ModelVolume::get_extruders() and
    force-keep any still-painted mixed virtual id, preventing a deleted
    painted row from being re-aliased into a silently wrong color.
  - MixedFilamentManager::refresh_display_colors promoted from private to
    public.
  - Sidebar::delete_filament gains skip_dependency_check / skip_update
    default params (back-compatible) so the cleanup path can trust
    compute_redundant_filaments and skip the dependency check.
  - MixedFilamentBatchDialog / MixedColorMatchHelpers updated for the flow.

2. Remove prusa_fdm_mixer demo code
  - Delete prusa_fdm_mixer.{cpp,hpp}, prusa_fdm_mixer_wrap.hpp, LICENSE,
    PROVENANCE (5 files).
  - Remove the ENABLE_PRUSA_FDM_MIXER_DEMO macro (Technologies.hpp) and all
    #if/#else branches, keeping the legacy path.
  - Update CMakeLists to drop the translation units; remove the A/B
    comparison test test_mix_model_ab_comparison.cpp.

3. Tests (test_mixed_filament.cpp +900 lines)
  - Cover compute_redundant_filaments paths: physical-only / cascade /
    survivor-floor / out-of-range / manual-pattern token resolution.
  - Differential [!shouldfail] "m1": pins the known divergence between
    compute and remove_physical on out-of-range literal tokens (currently
    unreachable in production, rejected by references_exceed_physical at
    every write path).
  - add_batch rejection paths, stable_id serialize round-trip, and an R3
    root-cause regression guard.
  - Add a num_physical == 0 guard (compute_redundant_filaments early-
    returns an empty result) plus its regression test.
…il viewpoints

MixedFilamentBatchDialog — UI overhaul aligned to Figma spec:
- Card-based layout (filament config / dual preview / color mapping);
  dialog width 760 -> 540.
- RoundedPreviewPanel: single wxBG_STYLE_PAINT handler draws rounded bg +
  thumbnail + corner mask + badge, replacing StaticBox + wxStaticBitmap
  (avoids MSW compositing quirks erasing rounded corners/badges).
- View combobox with 8 named viewpoints; thumbnail cache bucketed by
  viewpoint, lazily rendered per (viewpoint, plate). Original side now
  follows the selected viewpoint (was hardcoded to top).
- Plate nav with disabled state at first/last plate.
- Manual filament count defaults to physical filament count (capped [2,4]);
  add/remove buttons mirror MixedFilamentDialog.

GLCanvas3D — named thumbnail viewpoints (pure addition, zero impact on
existing call sites):
- New render_thumbnail overload takes ThumbnailView instead of bool
  use_top_view. bool and scoped enum don't implicitly convert, so
  overload resolution is unambiguous.
- New internal branch for named viewpoints (Camera::select_view +
  zoom_to_box + set_type). Existing call sites see view=Iso (default),
  routing to the original iso branch; the sole use_top_view=true caller
  (Plater's top_thumbnail_data for 3MF export) still hits the legacy
  top branch unchanged.
- New ThumbnailView.hpp keeps the enum out of the GLCanvas3D include chain.

Self-review fixes applied on top:
- Failed-match path routes through set_match_buttons_state(false) so the
  Start/Re-match enable mask follows m_match_completed.
- update_view() no longer discards the current viewpoint's match bucket
  on every switch (buckets stay valid across toggles; invalidation
  belongs in reset_match_preview / rebuild_match_thumb_cache).
- Preview size floors promoted to named constants matching panel size.
- Removed empty build_progress() no-op.
- std::array size derived from enum + static_assert locking enum order.
…s, tooltips

- Add inline "Stop Matching" button beside the progress bar (terminates
  the in-flight match without closing the dialog).
- Manual mode: numbered color swatch beside each filament combo, color
  tracks the selection; warn when one physical filament dominates >70%.
- Mode combo tooltip describes Auto vs Manual on hover.
- Mode row on a white host panel (fixes bg bleed-through).
- Plater entry: pre-check for empty model / <2 filaments via show_info.
- Dialog opens on the plater's currently selected plate.
- Icon touch-ups (icon_minus/plus/info) and zh_CN translations.
MixedFilamentBatchDialog:
- Inline numbered color badge into ComboBox SetIcon (arrow + badge
  composite) and remove m_filament_swatch[4]
- Fix wxStaticText bg colour on "Filament Setup" labels per
  wxWidgets pitfall §137 (SetForegroundColour + SetBackgroundColour)

Plater / Sidebar:
- Add BatchPhysicalDeletionGuard RAII to skip per-deletion panel
  rebuild and painting remap during batch cleanup; apply one
  composite remap after the loop
- Add ProgressDialog for batch-match apply flow, stepping
  0→20→40→60→80→100 so wxPD_AUTO_HIDE dismisses cleanly

Tab:
- Show advisory warning when Subdivide Mix Layer is active with
  layer height ≤0.1 mm (triggered on feature enable or
  layer-height change)

ThumbnailView:
- Extract ThumbnailView enum to lightweight header, add to
  CMakeLists, unify inline comments to English

Tests:
- Add shrink / tail-truncation remap tests (surviving id identity,
  tail→NONE mapping, compact→empty redundant_physical)

Housekeeping:
- i18n: 2 new zh_CN strings for Subdivide Mix Layer warnings
feat: Mixed Filament Phase 2 — batch color match dialog with multi-view preview
The object list's filament column still displayed old physical-slot
extruder IDs after confirming a batch color match because the earlier
on_filaments_change refresh ran before apply_batch_match_to_model did
the actual extruder remapping.

Root causes and fixes:

1. MixedFilamentBatchDialog::load_model_colors() was enumerating the
   extruder palette array instead of walking the model's actual painted
   extruder IDs via get_extruders(). This missed volume config-level
   extruder assignments (no MMU paint) and virtual mixed-filament IDs
   that were deduplicated against the physical palette. Rewrote to walk
   every MODEL_PART volume, look up each extruder's hex colour from the
   physical palette or the mixed-filament display_color table, and
   accumulate all matching extruder IDs per colour.

2. Recommended-mode confirm path (Plater.cpp) was losing pre-existing
   custom mixed filaments because set_num_filaments calls
   clear_custom_entries(). Fixed by snapshotting the old mixed list
   before the call, reloading custom entries from project_config
   afterwards to preserve stable_ids, and building an old→new virtual
   ID remap so on_filaments_change correctly translates existing painted
   mixed-IDs before apply_batch_match_to_model runs.

3. Added obj_list()->update_objects_list_filament_column() call after
   apply_batch_match_to_model completes so the object list reflects the
   post-remap extruder IDs.
fix: object list filament column not updating after batch color match
…ancel-aware rematch (#617)

* feat(mixed-filament): per-component ratio cap, Full Spectrum rename, safety hardening
Add max_component_percent through the color-match pipeline
(build_best_color_match_recipe, batch_match_model_colors,
recommend_best_filament_combo) to cap any single filament's share of a mix:
- RECOMMENDED: [0%, 70%] — product spec enforces no dominant component
- MANUAL: [15%, 100%] — near-pure allowed; over-70% surfaces as a post-match
  advisory via new check_manual_recipe_ratio(), listing all over-threshold IDs
Rename "CMYW" → "Full Spectrum"; add canonical preset constant
(kFullSpectrumPresetName) and auto-assign it to slots 1-4 after batch match
so the combobox shows the preset name instead of F1-F4.
Harden cleanup_unused_filaments_after_batch_match: promote the
redundant_physical descending invariant from assert() to a runtime check
(assert is compiled out under NDEBUG), with a per-deletion fallback that
keeps colours correct when violated.
Fix dangling reference in extract_model_colors — the ternary bound a
temporary MixedFilamentManager to a reference (use-after-scope UB); now a
raw pointer with null-check at the virtual-lookup site only.
Robustness: clamp mix_b_percent to [0,100], defer 64-color warning until
after build_ui, static_cast cleanup, null-guard preset_bundle at startup.
Harness review fixes (dev-guidelines): validate max_component_percent range
in build_best_color_match_recipe, replace residual C-style cast, document
m_match_running as UI-thread-only.

* fix: object list filament column not updating after batch color match

The object list's filament column still displayed old physical-slot
extruder IDs after confirming a batch color match because the earlier
on_filaments_change refresh ran before apply_batch_match_to_model did
the actual extruder remapping.

Root causes and fixes:

1. MixedFilamentBatchDialog::load_model_colors() was enumerating the
   extruder palette array instead of walking the model's actual painted
   extruder IDs via get_extruders(). This missed volume config-level
   extruder assignments (no MMU paint) and virtual mixed-filament IDs
   that were deduplicated against the physical palette. Rewrote to walk
   every MODEL_PART volume, look up each extruder's hex colour from the
   physical palette or the mixed-filament display_color table, and
   accumulate all matching extruder IDs per colour.

2. Recommended-mode confirm path (Plater.cpp) was losing pre-existing
   custom mixed filaments because set_num_filaments calls
   clear_custom_entries(). Fixed by snapshotting the old mixed list
   before the call, reloading custom entries from project_config
   afterwards to preserve stable_ids, and building an old→new virtual
   ID remap so on_filaments_change correctly translates existing painted
   mixed-IDs before apply_batch_match_to_model runs.

3. Added obj_list()->update_objects_list_filament_column() call after
   apply_batch_match_to_model completes so the object list reflects the
   post-remap extruder IDs.

* feat(mixed-filament): recommended-card tooltip, bordered row, discard-match confirm

- Per-row hover tooltip: localized color name + hex + TD (resolved by color
  family, not palette position — survives ΔE fallback reorder)
- Row border wraps swatch + name together (was: name field only)
- Cancel prompts discard confirmation only when a match result exists
- Skip refresh on method re-select (wxMSW combobox quirk)
- Consolidate Full Spectrum preset name into kFullSpectrumPresetName

* feat(mixed-filament): dedupe identical recipes, cancel-aware re-match, UX polish

- merge_duplicate_recipe_mappings: byte-identical non-pure recipes share one virtual slot
- distinguish recommend_best_filament_combo cancel vs no-combo (error_code 2)
- restore prior preview on failed re-match only when input unchanged
- triple-loop wb bound respects max_component_percent
- banners moved below mode row; canvas3D() -> get_view3D_canvas3D()

* refactor(mixed-filament): log null preset_bundle once in extract_model_colors

pb is loop-invariant; the per-iteration null check stayed (correct —
physical-colour path must not be skipped), but its warning fired once
per virtual-painted face. Hoist the warning above the loop, leave the
inner check as a silent continue.

* feat(mixed-filament): per-component ratio cap, Full Spectrum rename, safety hardening
Add max_component_percent through the color-match pipeline
(build_best_color_match_recipe, batch_match_model_colors,
recommend_best_filament_combo) to cap any single filament's share of a mix:
- RECOMMENDED: [0%, 70%] — product spec enforces no dominant component
- MANUAL: [15%, 100%] — near-pure allowed; over-70% surfaces as a post-match
  advisory via new check_manual_recipe_ratio(), listing all over-threshold IDs
Rename "CMYW" → "Full Spectrum"; add canonical preset constant
(kFullSpectrumPresetName) and auto-assign it to slots 1-4 after batch match
so the combobox shows the preset name instead of F1-F4.
Harden cleanup_unused_filaments_after_batch_match: promote the
redundant_physical descending invariant from assert() to a runtime check
(assert is compiled out under NDEBUG), with a per-deletion fallback that
keeps colours correct when violated.
Fix dangling reference in extract_model_colors — the ternary bound a
temporary MixedFilamentManager to a reference (use-after-scope UB); now a
raw pointer with null-check at the virtual-lookup site only.
Robustness: clamp mix_b_percent to [0,100], defer 64-color warning until
after build_ui, static_cast cleanup, null-guard preset_bundle at startup.
Harness review fixes (dev-guidelines): validate max_component_percent range
in build_best_color_match_recipe, replace residual C-style cast, document
m_match_running as UI-thread-only.

* feat(mixed-filament): recommended-card tooltip, bordered row, discard-match confirm

- Per-row hover tooltip: localized color name + hex + TD (resolved by color
  family, not palette position — survives ΔE fallback reorder)
- Row border wraps swatch + name together (was: name field only)
- Cancel prompts discard confirmation only when a match result exists
- Skip refresh on method re-select (wxMSW combobox quirk)
- Consolidate Full Spectrum preset name into kFullSpectrumPresetName

* feat(mixed-filament): dedupe identical recipes, cancel-aware re-match, UX polish

- merge_duplicate_recipe_mappings: byte-identical non-pure recipes share one virtual slot
- distinguish recommend_best_filament_combo cancel vs no-combo (error_code 2)
- restore prior preview on failed re-match only when input unchanged
- triple-loop wb bound respects max_component_percent
- banners moved below mode row; canvas3D() -> get_view3D_canvas3D()

* refactor(mixed-filament): log null preset_bundle once in extract_model_colors

pb is loop-invariant; the per-iteration null check stayed (correct —
physical-colour path must not be skipped), but its warning fired once
per virtual-painted face. Hoist the warning above the loop, leave the
inner check as a silent continue.

* feat(mixed-filament): allow cross-type mixes in manual batch mode

Add check_compatible through the recipe-search pipeline so manual
mode can create cross-type mixes (PLA+PETG). RECOMMENDED keeps the
same-type filter; the slice gate
(has_incompatible_mixed_filament_in_use) still blocks them at slice
time — widen in, strict out.

Also: empty error_message on cancel (code 2 is a silent rollback),
discard-confirm focus fix on wxMSW, banner spacing, RichMessageDialog
for the no-model/<2-filament prompts.

---------

Co-authored-by: kenshin627 <87zhaoxiaobo@163.com>
…-core hardening (#628)

* feat(mixed-filament): per-component ratio cap, Full Spectrum rename, safety hardening
Add max_component_percent through the color-match pipeline
(build_best_color_match_recipe, batch_match_model_colors,
recommend_best_filament_combo) to cap any single filament's share of a mix:
- RECOMMENDED: [0%, 70%] — product spec enforces no dominant component
- MANUAL: [15%, 100%] — near-pure allowed; over-70% surfaces as a post-match
  advisory via new check_manual_recipe_ratio(), listing all over-threshold IDs
Rename "CMYW" → "Full Spectrum"; add canonical preset constant
(kFullSpectrumPresetName) and auto-assign it to slots 1-4 after batch match
so the combobox shows the preset name instead of F1-F4.
Harden cleanup_unused_filaments_after_batch_match: promote the
redundant_physical descending invariant from assert() to a runtime check
(assert is compiled out under NDEBUG), with a per-deletion fallback that
keeps colours correct when violated.
Fix dangling reference in extract_model_colors — the ternary bound a
temporary MixedFilamentManager to a reference (use-after-scope UB); now a
raw pointer with null-check at the virtual-lookup site only.
Robustness: clamp mix_b_percent to [0,100], defer 64-color warning until
after build_ui, static_cast cleanup, null-guard preset_bundle at startup.
Harness review fixes (dev-guidelines): validate max_component_percent range
in build_best_color_match_recipe, replace residual C-style cast, document
m_match_running as UI-thread-only.

* feat(mixed-filament): recommended-card tooltip, bordered row, discard-match confirm

- Per-row hover tooltip: localized color name + hex + TD (resolved by color
  family, not palette position — survives ΔE fallback reorder)
- Row border wraps swatch + name together (was: name field only)
- Cancel prompts discard confirmation only when a match result exists
- Skip refresh on method re-select (wxMSW combobox quirk)
- Consolidate Full Spectrum preset name into kFullSpectrumPresetName

* feat(mixed-filament): dedupe identical recipes, cancel-aware re-match, UX polish

- merge_duplicate_recipe_mappings: byte-identical non-pure recipes share one virtual slot
- distinguish recommend_best_filament_combo cancel vs no-combo (error_code 2)
- restore prior preview on failed re-match only when input unchanged
- triple-loop wb bound respects max_component_percent
- banners moved below mode row; canvas3D() -> get_view3D_canvas3D()

* refactor(mixed-filament): log null preset_bundle once in extract_model_colors

pb is loop-invariant; the per-iteration null check stayed (correct —
physical-colour path must not be skipped), but its warning fired once
per virtual-painted face. Hoist the warning above the loop, leave the
inner check as a silent continue.

* feat(mixed-filament): per-component ratio cap, Full Spectrum rename, safety hardening
Add max_component_percent through the color-match pipeline
(build_best_color_match_recipe, batch_match_model_colors,
recommend_best_filament_combo) to cap any single filament's share of a mix:
- RECOMMENDED: [0%, 70%] — product spec enforces no dominant component
- MANUAL: [15%, 100%] — near-pure allowed; over-70% surfaces as a post-match
  advisory via new check_manual_recipe_ratio(), listing all over-threshold IDs
Rename "CMYW" → "Full Spectrum"; add canonical preset constant
(kFullSpectrumPresetName) and auto-assign it to slots 1-4 after batch match
so the combobox shows the preset name instead of F1-F4.
Harden cleanup_unused_filaments_after_batch_match: promote the
redundant_physical descending invariant from assert() to a runtime check
(assert is compiled out under NDEBUG), with a per-deletion fallback that
keeps colours correct when violated.
Fix dangling reference in extract_model_colors — the ternary bound a
temporary MixedFilamentManager to a reference (use-after-scope UB); now a
raw pointer with null-check at the virtual-lookup site only.
Robustness: clamp mix_b_percent to [0,100], defer 64-color warning until
after build_ui, static_cast cleanup, null-guard preset_bundle at startup.
Harness review fixes (dev-guidelines): validate max_component_percent range
in build_best_color_match_recipe, replace residual C-style cast, document
m_match_running as UI-thread-only.

* feat(mixed-filament): recommended-card tooltip, bordered row, discard-match confirm

- Per-row hover tooltip: localized color name + hex + TD (resolved by color
  family, not palette position — survives ΔE fallback reorder)
- Row border wraps swatch + name together (was: name field only)
- Cancel prompts discard confirmation only when a match result exists
- Skip refresh on method re-select (wxMSW combobox quirk)
- Consolidate Full Spectrum preset name into kFullSpectrumPresetName

* feat(mixed-filament): dedupe identical recipes, cancel-aware re-match, UX polish

- merge_duplicate_recipe_mappings: byte-identical non-pure recipes share one virtual slot
- distinguish recommend_best_filament_combo cancel vs no-combo (error_code 2)
- restore prior preview on failed re-match only when input unchanged
- triple-loop wb bound respects max_component_percent
- banners moved below mode row; canvas3D() -> get_view3D_canvas3D()

* refactor(mixed-filament): log null preset_bundle once in extract_model_colors

pb is loop-invariant; the per-iteration null check stayed (correct —
physical-colour path must not be skipped), but its warning fired once
per virtual-painted face. Hoist the warning above the loop, leave the
inner check as a silent continue.

* feat(mixed-filament): allow cross-type mixes in manual batch mode

Add check_compatible through the recipe-search pipeline so manual
mode can create cross-type mixes (PLA+PETG). RECOMMENDED keeps the
same-type filter; the slice gate
(has_incompatible_mixed_filament_in_use) still blocks them at slice
time — widen in, strict out.

Also: empty error_message on cancel (code 2 is a silent rollback),
discard-confirm focus fix on wxMSW, banner spacing, RichMessageDialog
for the no-model/<2-filament prompts.

* fix(mixed-filament): harden color-match core, align batch dialog with PRD

- Fix data race on s_compat_loaded (atomic DCLP) and toupper UB on non-ASCII filament types
- Add max_component_percent [50,100] validation in batch_match_model_colors
- Switch batch match to full project palette (load_palette_colors); align ΔE grade thresholds with PRD
- Set icon bg for wxMSW transparency; refine banner spacing

* feat(mixed-filament): nozzle-aware preset + dark-mode preview polish

- Replace inline `kFullSpectrumPresetName` constant with
  `full_spectrum_preset_name()`; resolves the canonical 0.4 SKU and falls
  back when no matching preset exists for the current nozzle_diameter.
  Future nozzle variants ship as preset JSON only — no code change.
- Make MixedFilament preview theme-aware: panel bg #F5F5F5 → #E7E7E7,
  add #E7E7E7→#54545B and #939393→#000000 dark mappings (StateColor),
  blend thumbnail alpha against the resolved bg so cached bitmaps follow
  the active theme instead of baking in a hardcoded light bg.
- Rename "Batch Match" → "Color Mixing Match" with explanatory tooltip;
  parameterize the mix-ratio tooltip/warning via wxString::Format
  (%d%%–%d%%) instead of the X%–Y% placeholder.
- Update zh_CN translations for new/changed strings.
The RoundedPreviewPanel used a wxImage color-mask corner overlay
approach that did not render correctly on MSW — the preview images
appeared with square corners regardless of the mask.

Replaced the mask-based approach with OS-level window region clipping
(SetWindowRgn / CreateRoundRectRgn on Windows, wxRegion::Intersect on
macOS/Linux). The panel is physically shaped to a rounded rectangle by
the OS, so corners are transparent regardless of what is painted.
- Manual-mode combo box and combo icon now use GetFilamentColorIcon
  for multi-color aware filament swatches (segment/gradient) instead
  of solid-color get_extruder_color_icon.
- Mapping legend source swatches render dual-color segment/gradient
  blocks for pure-recipe mappings when the target physical filament
  has multi-color data, matching the visual style used elsewhere.
- Read m_physical_multi_colors and m_physical_color_modes from
  project_config at dialog construction; resize to match physical
  filament count for safe downstream index access.
- Fix tooltip binding: use src_bmp_ctrl (wxStaticBitmap) instead of
  raw wxBitmap pointer which is not a window and cannot show tooltips.
…lish

- Keep-aware batch remap: map survivors by position in kept set so manual
  subsets like [2,6,8,10] keep their painting instead of being truncated to
  NONE (PresetBundle, Plater).
- Preview: substitute every source_extruder_id per mapping so shared-color
  slots all render the matched color.
- Confirm gate: warn at commitment when a match would exceed the 64-filament
  cap (predict_slot_overflow).
- Performance: freeze sidebar across the batch deletion loop; per-deletion
  progress via on_progress callback (~10.7s vs ~29.9s).
- UX: nozzle != 0.4 blocks recommended mode; strict <3/<5 grade bands;
  add-row cap tracks physical spool count; relayout_scrolled_content helper.
- Tests: +835 lines covering remap shape matrix, apply target==src skip,
  preview multi-slot, redundant-set non-contiguous subset.
…nd persist across mode switch

- Change check_manual_recipe_ratio() to report target_filament_id (legend badge
  number) instead of physical slot index, so the warning lists the mapped
  filament IDs the user actually sees.
- Re-run ratio check when switching back to Manual mode so the advisory banner
  is restored without requiring a re-match (m_match_completed stays true across
  mode toggles).
…or preview rounded corners

The RoundedPreviewPanel used SetWindowRgn (MSW) / SetShape (macOS/Linux)
to physically clip child wxPanel to a rounded rect.  This caused three
blocking defects:

  N1 (MSW): ~RoundedPreviewPanel called DeleteObject on an HRGN whose
      ownership had been transferred to the OS by SetWindowRgn(…, TRUE).
      Double-free of a GDI handle → non-deterministic heap corruption.
  N2 (MSW): apply_rounded_shape (wxEVT_SIZE) called DeleteObject on
      the cached HRGN from the previous call, which was also already
      owned by the OS.  Triggered on every resize.
  N3 (macOS/Linux): SetShape(const wxRegion&) is only defined on
      wxNonOwnedWindowBase (top-level windows with wxFRAME_SHAPED).
      Calling it on a child wxPanel is a hard compile error.

Replace the physical-clip approach with a paint-layer solution:

  1. Fill the entire panel with white (parent card background).
  2. Build a rounded-rect wxRegion via a mask bitmap, then call
     SetDeviceClippingRegion — standard wxDC API, child-window safe
     on all platforms, no GDI ownership transfer.
  3. Draw the rounded background (#E7E7E7) + thumbnail + badge.
     The four corners remain white, reading as transparent over the
     parent card.

Also restore the wxEVT_DPI_CHANGED handler (lost in the PR rewrite) to
re-rasterize the placeholder SVG at the new DPI on monitor switch.

No other functional changes — the Partial 2 multi-color rendering code
(dual-color swatches, GetFilamentColorIcon, legend source-switch logic)
is untouched.
…leCorner

Fix mixed filament shotcut circle corner
- close data race: pass check_compatible=false in the RECOMMENDED worker
  call site (build_compatibility_matrix read preset_bundle->filament_presets
  unsynchronized); safe because RECOMMENDED palette is always a single
  same-material Full Spectrum preset, and the slice gate still backstops
  real incompatibility
- validate min/max percent range in recommend_best_filament_combo (reject
  with a log instead of silently scoring with an empty window)
- assert non-zero stable_id contract in allocate_stable_id (debug-only
  guard; max(1,...) already guarantees it)
- revise predict_slot_overflow comment to match actual execution order
  (cleanup runs after add_batch, so it is an upper bound)
- UI: keep progress bar as native wxGauge; fix dark-mode button colors
  (Confirm/Tab text #FFFFFF -> #FEFEFE stays white via dark map)
- copy: Filament Setup -> Filament Setup (CMYG); nozzle-unsupported hint
  rewritten; warning id list "{x,y}" -> "x, y"
- progress dialog title/body reworded (Applying color match -> Loading)
- add 0.10 Color Mixing @Snapmaker U1 (0.4 nozzle) process preset
- document wxStaticText bg-inheritance pitfall in AGENTS.md
- zh_CN translations for the new strings
…se2_pr

Conflict in MixedFilamentBatchDialog::check_manual_recipe_ratio resolved by
taking upstream's no-dedup version (target ids from non-pure mappings are
already unique & ascending via next_virtual_id++, preserved through
merge_duplicate_recipe_mappings), plus keeping the target_filament_id == 0
guard (skip unmapped rows that have no legend badge to report).
…flict"

Rename both Subdivide Mixing Layers warning dialogs from "Warning" to
"Configuration Conflict" and drop the wxICON_WARNING icon. Add the
zh_CN translation.
…2_pr

fix(mixed-filament): preserve painting for non-contiguous manual palette subsets
Kenshin627 and others added 23 commits August 7, 2026 14:32
Batch color match only remapped object/volume extruders and ignored
layer_config_ranges entirely. A layer object's filament stayed on its old
physical slot after the palette was rewritten, so when cleanup deleted the
unreferenced physical filaments the layer was reset to default — surfacing
as "layer filament >N lost / reset to default".

Include layer_config_ranges alongside volumes:
- apply_batch_match_to_model: add a Level-3 pass that remaps each layer
  object's extruder with the same extruder_remap used for volumes;
- load_model_colors: collect layer-object extruders as match sources too,
  so a layer whose filament no volume paints still gets matched and remapped.
Address the review findings on the modifier/layer batch-match fix
(PR #674). No behavioural change in the common (single-mapping) case;
the changes below make the apply path correct under remap chains and
remove ineffective/cosmetic churn.

apply_batch_match_to_model (MixedColorMatchHelpers.cpp):
- Snapshot the object's extruder ONCE before iterating volumes instead
  of re-reading ModelVolume::extruder_id() per volume. extruder_id()
  falls back to the OBJECT config, which this loop rewrites mid-
  iteration, so a later inheriting volume would resolve the just-written
  value and — under a remap chain like {5->2, 2->7} — diverge from an
  earlier one (order-dependent). All inheriting volumes (part or
  modifier) now follow the same target. Drop the now-unreachable
  else-branch and the misleading comment that claimed it handled
  "inheriting volumes with different targets".
- Remove the unused `Print& print` parameter (the function walks
  wxGetApp().model(), never `print`); sync the header and the lone
  caller in Plater.cpp.

Dead-code cleanup (MixedFilamentBatchDialog.cpp / MixedColorMatchHelpers.cpp):
- Revert the modifier/layer edits made to the two DEAD functions
  load_model_colors and extract_model_colors (both have zero callers;
  the ctor calls load_palette_colors instead). Those edits had no
  runtime effect and only inflated the diff. The functions are kept
  (marked "legacy / future use") and restored to their pre-PR
  MODEL_PART-only form. The live layer fix lives entirely in
  apply_batch_match_to_model's Level-3 pass; load_palette_colors already
  enumerates every physical slot as a match source, so physical layer
  filaments are covered without the dead-function edit.

Tests (tests/libslic3r/test_mixed_filament.cpp):
- Add 4 Catch2 cases for the Level-2/Level-3 apply path. The GUI
  function cannot be linked from the test binary, so they mirror the
  loop over the real libslic3r Model API — same approach as the existing
  build_apply_extruder_remap tests:
  * extruder_id() falls back to the LIVE object config (pure libslic3r,
    pins why the snapshot is needed);
  * an inheriting modifier follows the object consistently;
  * a modifier with its own extruder is remapped alone;
  * layer_config_ranges: a hit is remapped, a miss is left untouched.
Call update_data() instead of update_gizmos_on_off_state() after
update_volumes_colors_by_extruder() so the active painting gizmo
picks up new colors (legend + painted facets) without being
deactivated.
…idle height

Idle footer leaked ~12 DIP because the old progress row (gauge + Stop
Matching) only hid its widgets — the sizer's spacers and border still
occupied vertical space. Wrap the progress row plus its lower hairline
in a wxBoxSizer block and toggle it as a unit via ShowItems(), so the
whole block — widgets, spacers, and gaps — collapses to 0 in idle.
Matching-state layout (44-DIP row, divider) is unchanged; Layout() is
still called after every toggle so Cancel/Confirm stay pinned.

Also tighten preview-card spacing: 8-DIP gap to/from the divider via
AddSpacer (replacing the wxTOP border on the crow), and adjust the
crow inter-element gaps to match Figma.
…2_pr

fix: mixed-filament issues including painting remap and UI updates
…t gate

- normalize_color_match_weights: guard against count == 0. The old code
  wrote out[0] on an empty vector in the remainder-fill loop (heap
  out-of-bounds write, UB) when count == 0 and weights was non-empty.
  Return an empty vector up front; all call sites already tolerate it.
- MixedFilamentBatchDialog: record the user's manual ComboBox selection in
  m_filament_selections on wxEVT_COMBOBOX, so the failed/cancelled re-match
  restore gate (input_intact) can detect that the manual input changed
  instead of always restoring a stale preview.
- zh_CN: fix stray braces in the "mix ratios" format string ({%s} -> %s);
  specifier count still matches the msgid.
- Snapmaker.json: drop the dangling duplicate "0.10 Color Mixing @Snapmaker
  U1 (0.4 Nozzle)" process entry whose sub_path points to a non-existent
  file (case mismatch); the lowercase entry and its file are retained.
- MixedFilament.hpp: add #pragma once (keep existing include guard).
- Replace C-style casts with static_cast in MixedColorMatchHelpers.cpp and
  MixedColorMatchPanel.cpp.
…refs on cleanup

- MixedFilamentBatchDialog: add slot_match_color to derive a dual-color slot's
  effective primary color from filament_multi_colors (PrimaryColor rule); apply it
  to the thumb cache, original-plate thumbs, palette loading and background match
  so slots with an empty filament_colour are matched by their real color instead of
  being skipped or rendered black.
- MixedFilamentBatchDialog: render one legend row per source color for merged
  (mixed-recipe) mappings via merged_model_indices, each with its own swatch and
  per-row ΔE (color_delta_e00), so merged colors are no longer hidden under the
  first source color.
- Plater: in batch-match cleanup, guard config-level "extruder" references
  (object/volume/layer) so mixed rows referenced only from config are not flagged
  redundant, and remap those references with mixed_deletion_remap alongside painting.
- tests: pin delete-list dedupe and dual-color primary derivation; document the
  known cascade-aware config remap gap as an intentionally-failing ([.]) test.
… painted rows

Plater.cpp cleanup_unused_filaments_after_batch_match: the get_extruders
force-keep is not absolute — a mixed row whose components reference a
deleted physical is still erased by remove_physical_filament, and its
stranded painting falls back to build_filament_id_remap's pair-fallback
(possibly a wrong survivor). Comment-only change; no behavior.
The batch color match remapped the parent object's filament only inside
the "volume inherits the object's extruder" branch of
apply_batch_match_to_model. For multi-part objects where EVERY volume
owns its own extruder (e.g. Creality 3mf exports — object extruder = 1,
parts 1..6 each carry extruder 1,2,3,4,5,5), no volume inherits, so that
branch never ran and the parent row stayed on its stale filament while
the volumes were remapped — "parent object's filament not mapped".

The BambuStudio 3mf (single volume, no per-volume extruder) worked
because the volume inherited the object's extruder, which the branch
rewrote once.

Fix: remap the object-level extruder independently, before iterating
volumes — write the object whenever its extruder is in the remap table.
Inheriting volumes then resolve through the already-rewritten object
config, so the volume loop no longer needs the inheritance branch.
Behaviour for the inheriting case is unchanged.
feat: Enhance mixed filament batch matching with UI and algorithm updates
fix(mixed-filament): remap parent object filament in batch color match
…l-count expansion

Rebuild the old->new virtual-id remap whenever the physical filament count
changes in the batch-match recommended flow, not only when the mixed-row list
differs. A count change shifts every mixed row's virtual id even when rows are
structurally identical (MixedFilament::operator== ignores display_color), so the
previous check skipped the remap and left painted triangles and config
"extruder" keys on stale ids (e.g. mixed row vid 3 -> 5 on a 2->4 expansion).

- Sidebar batch-match branch: run update_mixed_filament_id_remap when
  current_count != target_count OR the row list differs.
- Remap object/volume/layer "extruder" config keys with the same state_map as
  the triangle remap, scoped to the batch-match branch only; other remap paths
  (row delete/enable, manual add/remove) are intentionally left unchanged.
- Add regression test: expanding 2 physical + 1 mixed row to 4 physicals with a
  structurally identical row list still remaps mixed vid 3 -> 5
  (verified remap=[0,1,2,5], stable_id match, 5 assertions).
The batch color match remapped the parent object's filament only inside
the "volume inherits the object's extruder" branch of
apply_batch_match_to_model. For multi-part objects where EVERY volume
owns its own extruder (e.g. Creality 3mf exports — object extruder = 1,
parts 1..6 each carry extruder 1,2,3,4,5,5), no volume inherits, so that
branch never ran and the parent row stayed on its stale filament while
the volumes were remapped — "parent object's filament not mapped".

The BambuStudio 3mf (single volume, no per-volume extruder) worked
because the volume inherited the object's extruder, which the branch
rewrote once.

Fix: remap the object-level extruder independently, before iterating
volumes — write the object whenever its extruder is in the remap table.
Inheriting volumes then resolve through the already-rewritten object
config, so the volume loop no longer needs the inheritance branch.
Behaviour for the inheriting case is unchanged.
Batch-match and cleanup paths both remapped a deleted/expired mixed-row
"extruder" config key by erasing it on objects/volumes while writing 0 on
layers. A missing key makes ModelConfig::extruder() (opt_int on an absent
option) a nullptr dereference for any reader that does not check has() first
(e.g. GUI_ObjectList's layer-range delete path), and erasing diverged from
the out-of-range normalization in update_filament_values_for_items which
writes an explicit 0.

Unify all three config levels (object/volume/layer) on cfg.set("extruder", 0):
the key stays present so no unprotected reader can crash, the explicit 0
matches the existing GUI normalization, and objects/volumes with 0 still
resolve to "default" via ModelVolume::extruder_id()'s inherit-from-object
fallback. Drop the now-redundant is_layer parameter in both remap lambdas.

Also copy batch_remap instead of borrowing PresetBundle::last_filament_id_remap()
so the config remap block no longer depends on on_filaments_change consuming
(moving + clearing) the internal buffer before the reference is used.
fix: rebuild id remap and sync config keys on physical-count expansion
…ssage

Replace the hardcoded "0.4 mm nozzle" hint with a generic smaller-size
nozzle wording, since any preset-defined nozzle size is auto-supported.
Sync the zh_CN translation to match the updated source string.
fix: reword auto color-match nozzle-diameter block message
… manual edit

In manual mode, after a match predicted a 64-slot overflow (red banner), editing a
filament combo re-entered check_manual_recipe_ratio(), whose display_warning() call hid
the overflow error and replaced it with the less-serious ratio hint. The overflow-wins-
over-ratio invariant was only enforced in handle_batch_match_result, not on the
re-evaluation paths (on_manual_selection_changed, on_method_changed).

Centralize the priority in a new try_show_slot_overflow_advisory() helper and call it
at the top of check_manual_recipe_ratio so every entry point honours it. Also lets
on_method_changed re-show the overflow advisory when toggling back to manual.
fix(mixed-filament): keep slot-overflow banner over ratio advisory on manual edit
@github-actions

Copy link
Copy Markdown

Documentation validation failed

🔗 Link Validation Errors

📄 doc/developer-reference\Built-in-placeholders-variables.md:

  • Line 93: home#process-settings - Fragment does not exist
  • Line 93: home#material-settings - Fragment does not exist

📄 doc/developer-reference\How-to-wiki.md:

  • Line 44: home#material-settings - Fragment does not exist
  • Line 45: home#process-settings - Fragment does not exist
  • Line 46: home#prepare - Fragment does not exist
  • Line 47: home#calibrations - Fragment does not exist
  • Line 50: home#process-settings - Fragment does not exist
  • Line 50: home#support-settings - Fragment does not exist
  • Line 50: home#others-settings - Fragment does not exist

📄 doc/Home.md:

  • Line 14: Precise-wall.md - File does not exist
  • Line 17: extrusion-rate-smoothing.md - File does not exist
  • Line 33: adaptive-pressure-advance.md - File does not exist

📄 doc/Tab.cpp:

  • Line 2398: fill-patterns - File does not exist
  • Line 2401: fill-patterns - File does not exist
  • Line 2582: multimaterial_settings_prime_tower#brim-chamfer - Fragment does not exist
  • Line 2583: multimaterial_settings_prime_tower#brim-chamfer-max-width - Fragment does not exist
  • Line 2595: multimaterial_settings_prime_tower#wall-gap - Fragment does not exist
  • Line 2609: multimaterial_settings_ooze_prevention#delta-temperature - Fragment does not exist

🖼️ Image Validation Errors

📄 doc/developer-reference\How-to-create-profiles.md:

  • Line 142: [Markdown] Alt text "Help menu" ≠ filename "go-to-configuration-folder"
  • Line 144: [Markdown] Alt text "Delete system folder" ≠ filename "profile-delete-system-folder"

fix(mixed-filament): signal cancel before dialog close to avoid UI-thread join stall
@github-actions

Copy link
Copy Markdown

Documentation validation failed

🔗 Link Validation Errors

📄 doc/developer-reference\Built-in-placeholders-variables.md:

  • Line 93: home#process-settings - Fragment does not exist
  • Line 93: home#material-settings - Fragment does not exist

📄 doc/developer-reference\How-to-wiki.md:

  • Line 44: home#material-settings - Fragment does not exist
  • Line 45: home#process-settings - Fragment does not exist
  • Line 46: home#prepare - Fragment does not exist
  • Line 47: home#calibrations - Fragment does not exist
  • Line 50: home#process-settings - Fragment does not exist
  • Line 50: home#support-settings - Fragment does not exist
  • Line 50: home#others-settings - Fragment does not exist

📄 doc/Home.md:

  • Line 14: Precise-wall.md - File does not exist
  • Line 17: extrusion-rate-smoothing.md - File does not exist
  • Line 33: adaptive-pressure-advance.md - File does not exist

📄 doc/Tab.cpp:

  • Line 2398: fill-patterns - File does not exist
  • Line 2401: fill-patterns - File does not exist
  • Line 2582: multimaterial_settings_prime_tower#brim-chamfer - Fragment does not exist
  • Line 2583: multimaterial_settings_prime_tower#brim-chamfer-max-width - Fragment does not exist
  • Line 2595: multimaterial_settings_prime_tower#wall-gap - Fragment does not exist
  • Line 2609: multimaterial_settings_ooze_prevention#delta-temperature - Fragment does not exist

🖼️ Image Validation Errors

📄 doc/developer-reference\How-to-create-profiles.md:

  • Line 142: [Markdown] Alt text "Help menu" ≠ filename "go-to-configuration-folder"
  • Line 144: [Markdown] Alt text "Delete system folder" ≠ filename "profile-delete-system-folder"

@LiuLikeQian
LiuLikeQian merged commit 9e42e78 into main Aug 13, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants