From 4acdcd72031c585d663826e2cf0a950e5a98dd79 Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 28 Apr 2026 12:29:19 +0200 Subject: [PATCH] =?UTF-8?q?=E2=97=8F=20feat(slicer):=20multi-color=20slici?= =?UTF-8?q?ng=20+=20per-plate=20filament=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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). --- backend/app/api/routes/library.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/app/api/routes/library.py b/backend/app/api/routes/library.py index 5079fd9e3..ccadf6283 100644 --- a/backend/app/api/routes/library.py +++ b/backend/app/api/routes/library.py @@ -2899,6 +2899,14 @@ async def slice_and_persist_as_archive( if used_embedded_settings: 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( printer_id=source_archive.printer_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_time_seconds=result.print_time_seconds, 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, nozzle_diameter=source_archive.nozzle_diameter, sliced_for_model=source_archive.sliced_for_model,