Browse Source

● feat(slicer): multi-color slicing + per-plate filament discovery

  The slice modal previously rendered exactly one filament dropdown and
  silently truncated multi-color 3MFs to a single profile, producing wrong
  colours on every multi-filament print. End-to-end fix across sidecar,
  backend, and frontend.

  Sidecar (orca-slicer-api / bambuddy/profile-resolver, separate commit):
    - /slice accepts up to 16 repeated filamentProfile parts; slicing
      service materializes each and joins paths with `;` for
      --load-filaments.
    - /profiles/bundled emits filament_type and filament_colour per leaf
      so the bundled tier carries metadata into the modal.

  Bambuddy backend:
    - SliceRequest gains filament_presets: list[PresetRef]. Validator
      accepts three shapes (multi-color array, source-aware singular,
      legacy bare-int id) and lands them all on a populated array before
      the route handler runs — fully backwards-compatible.
    - SlicerApiService.slice_with_profiles takes filament_profile_jsons:
      list[str] and sends one filamentProfile multipart part per profile
      (in submission order) so the sidecar receives N profiles cleanly.
    - New service slice_preview runs the sidecar's slice_without_profiles
      against an unsliced project file's embedded settings, parses the
      result's slice_info.config, and returns the canonical per-plate
      filament list. Cached by (kind, source_id, plate_id, content_hash)
      with LRU eviction at 256 entries, per-key asyncio.Lock prevents
      thundering-herd; transient sidecar failures are NOT cached so they
      retry naturally; parse failures ARE cached (deterministic property
      of the input, no point re-running).
    - /filament-requirements endpoint chain: slice_info.config (existing,
      sliced files) → preview-slice (new, unsliced project files) →
      project_settings.config + painted-face heuristic with 5% noise
      threshold (sidecar-down fallback).
    - threemf_tools gains extract_project_filaments_from_3mf and
      extract_plate_extruder_set_from_3mf — the latter unions object
      top-level extruder, per-part overrides, and painted-face quadtree
      leaves (1-E nibbles in paint_color attrs of <triangle> elements
      inside per-object .model files).
    - Cloud preset listing no longer fetches per-preset detail (Bambu's
      rate limit at ~10/sec returns 429 on every request for users with
      50+ presets). Unified-listing dedup pass instead backfills metadata
      cross-tier so a cloud entry that wins dedup over a same-named local
      entry inherits the local's filament_type / filament_colour.
    - slice_and_persist_as_archive now reads filament_type / filament_color
      from the SLICED OUTPUT's slice_info.config (via ThreeMFParser, which
      already gates on used_g > 0) instead of inheriting from the unsliced
      source archive. Without this, archive cards for sliced multi-color
      prints showed every project-wide AMS slot — 18 swatches for a
      2-color print — instead of just the filaments actually consumed.

  Frontend:
    - SliceModal multi-step: plate-picker first when the source is a
      multi-plate 3MF, then preset dropdowns. One filament dropdown per
      AMS slot the plate actually uses, each pre-picked by metadata
      match against user's local + standard presets via existing
      colorsAreSimilar / normalizeColorForCompare utils.
    - SliceModal-only tier priority is now local → cloud → standard
      (was cloud → local → standard). Other consumers of /slicer/presets
      keep the existing cloud-first order.
    - Submits filament_presets array; backfills the legacy singular
      filament_preset from the array's first entry for stale-tab
      compatibility.
    - i18n keys added across all 8 locales: slice.filamentSlot,
      slice.tier.{local,cloud,standard}, slice.cloud.{notAuthenticated,
      expired,unreachable}, slice.noPresetsForSlot,
      slice.allPresetsRequired (en + de fully translated; six others
      seeded with English copies pending native translation, matching
      the project's existing flow).
maziggy 4 weeks ago
parent
commit
4acdcd7203
1 changed files with 10 additions and 2 deletions
  1. 10 2
      backend/app/api/routes/library.py

+ 10 - 2
backend/app/api/routes/library.py

@@ -2899,6 +2899,14 @@ async def slice_and_persist_as_archive(
     if used_embedded_settings:
     if used_embedded_settings:
         metadata["used_embedded_settings"] = True
         metadata["used_embedded_settings"] = True
 
 
+    # Prefer the actually-used filament list from the sliced output's
+    # slice_info.config (parsed_metadata.filament_* — only entries with
+    # used_g > 0). Falling back to the source_archive's list would
+    # surface every project-wide AMS slot, including ones the picked
+    # plate doesn't use (16+ swatches on the card for a 2-color print).
+    new_filament_type = parsed_metadata.get("filament_type") or source_archive.filament_type
+    new_filament_color = parsed_metadata.get("filament_color") or source_archive.filament_color
+
     new_archive = PrintArchive(
     new_archive = PrintArchive(
         printer_id=source_archive.printer_id,
         printer_id=source_archive.printer_id,
         project_id=source_archive.project_id,
         project_id=source_archive.project_id,
@@ -2912,8 +2920,8 @@ async def slice_and_persist_as_archive(
         print_name=(source_archive.print_name or base_name) + " (re-sliced)",
         print_name=(source_archive.print_name or base_name) + " (re-sliced)",
         print_time_seconds=result.print_time_seconds,
         print_time_seconds=result.print_time_seconds,
         filament_used_grams=result.filament_used_g or None,
         filament_used_grams=result.filament_used_g or None,
-        filament_type=source_archive.filament_type,
-        filament_color=source_archive.filament_color,
+        filament_type=new_filament_type,
+        filament_color=new_filament_color,
         layer_height=source_archive.layer_height,
         layer_height=source_archive.layer_height,
         nozzle_diameter=source_archive.nozzle_diameter,
         nozzle_diameter=source_archive.nozzle_diameter,
         sliced_for_model=source_archive.sliced_for_model,
         sliced_for_model=source_archive.sliced_for_model,