From 6497eefe607bfb877c29d5636c2272e71c075d9e Mon Sep 17 00:00:00 2001 From: maziggy Date: Tue, 28 Apr 2026 12:29:35 +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). --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887816d21..55de171f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.4b1] - Unreleased ### Added -- **Multi-color slicing in the Slice modal, with per-plate filament discovery for unsliced project files** — Initial slice support assumed a single filament profile per slice; multi-color 3MFs were silently truncated to the first slot, producing wrong colours on every non-trivial print. The Slice modal now (1) opens a plate-picker step first when the source is a multi-plate 3MF, (2) renders one filament dropdown per AMS slot the picked plate actually uses, with each dropdown auto-populated against the user's local + standard presets by `(filament_type, filament_colour)` match, and (3) submits the user's picks as an ordered `filament_presets: PresetRef[]` array which is forwarded as repeated `filamentProfile` multipart parts to the slicer sidecar (the CLI joins them with `;` for `--load-filaments`). **Per-plate filament list source-of-truth chain**: for a sliced archive the modal reads `Metadata/slice_info.config` directly (existing path); for an unsliced project file (where `slice_info.config` is empty until Bambu Studio actually slices), the new `slice_preview` service runs a fast preview-slice via the sidecar's `slice_without_profiles` (the project's embedded settings drive the slice; we throw away the gcode and only parse the resulting slice_info), and the result is cached by `(kind, source_id, plate_id, content_hash)` with LRU eviction at 256 entries — repeat opens of the same plate are instant. If the sidecar isn't reachable the modal falls back to a heuristic that reads `Metadata/project_settings.config` for the AMS slot config and intersects it with the plate's painted-face data (`paint_color` quadtree leaves on per-object .model files, scanned with a 5% noise threshold to drop single-leaf edit accidents). **SliceModal-only tier priority is now `local → cloud → standard`** (was `cloud → local → standard`): imported profiles win because they carry parsed type/colour metadata in the response, while cloud entries don't (the per-preset detail endpoint rate-limits at ~10/sec per token and 50+ parallel fetches returned 429 on every request). The unified-listing endpoint's dedup pass now backfills metadata cross-tier — if a cloud entry wins dedup over a same-named local entry, the cloud entry inherits the local's `filament_type` / `filament_colour` so the Slice modal's metadata-aware pre-pick keeps working for users who have presets both cloud-synced and locally imported. Other consumers of `/slicer/presets` (Profiles page, etc.) retain the existing cloud-first dedup. **Sidecar** (orca-slicer-api fork, `bambuddy/profile-resolver` branch): `/slice` now accepts up to 16 repeated `filamentProfile` parts (was hard-capped at 1), the slicing service materializes each as `filament_N.json` and joins paths into a single `--load-filaments "a.json;b.json;c.json"` invocation; `/profiles/bundled` listing was extended with `filament_type` and `filament_colour` per leaf so the bundled tier carries metadata into the modal. **Backwards compatibility**: `SliceRequest` schema accepts three shapes — legacy `filament_preset_id: int`, source-aware singular `filament_preset: PresetRef`, multi-color array `filament_presets: list[PresetRef]` — the validator promotes any of them into a populated `filament_presets` list before the route handler runs, and stale browser tabs from before this change keep working unchanged. **Permissions**: no new endpoint paths added; the preview-slice runs inside `/filament-requirements` (gated on `LIBRARY_READ` / `ARCHIVES_READ`) and the multi-filament dispatch runs inside `POST /slice` (gated on `LIBRARY_UPLOAD`) — no auth surface widened. **Tests**: 6 schema tests for `SliceRequest` covering the multi-filament list shape and legacy-vs-new precedence; 9 unit tests for `slice_preview` covering happy path, content-hash invalidation, sidecar-failure no-cache-poison, concurrent-call thundering-herd guard via per-key `asyncio.Lock`, and LRU eviction-with-lock-cleanup; 15 unit tests for `extract_project_filaments_from_3mf` (5 cases) and `extract_plate_extruder_set_from_3mf` (10 cases including the 60/40 painted-threshold pin); a multi-filament wire-format test on `slice_with_profiles` pinning that N filament profiles produce N repeated multipart parts in submission order; 22 frontend SliceModal tests covering the plate picker step, multi-color rendering, metadata-aware pre-pick, manual slot override, archive-vs-library routing, and the new tier order. Localised across all 8 UI languages (English + German fully translated, the six others seeded with English copies pending native translation per the project's existing flow). +- **Multi-color slicing in the Slice modal, with per-plate filament discovery for unsliced project files** — Initial slice support assumed a single filament profile per slice; multi-color 3MFs were silently truncated to the first slot, producing wrong colours on every non-trivial print. The Slice modal now (1) opens a plate-picker step first when the source is a multi-plate 3MF, (2) renders one filament dropdown per AMS slot the picked plate actually uses, with each dropdown auto-populated against the user's local + standard presets by `(filament_type, filament_colour)` match, and (3) submits the user's picks as an ordered `filament_presets: PresetRef[]` array which is forwarded as repeated `filamentProfile` multipart parts to the slicer sidecar (the CLI joins them with `;` for `--load-filaments`). **Per-plate filament list source-of-truth chain**: for a sliced archive the modal reads `Metadata/slice_info.config` directly (existing path); for an unsliced project file (where `slice_info.config` is empty until Bambu Studio actually slices), the new `slice_preview` service runs a fast preview-slice via the sidecar's `slice_without_profiles` (the project's embedded settings drive the slice; we throw away the gcode and only parse the resulting slice_info), and the result is cached by `(kind, source_id, plate_id, content_hash)` with LRU eviction at 256 entries — repeat opens of the same plate are instant. If the sidecar isn't reachable the modal falls back to a heuristic that reads `Metadata/project_settings.config` for the AMS slot config and intersects it with the plate's painted-face data (`paint_color` quadtree leaves on per-object .model files, scanned with a 5% noise threshold to drop single-leaf edit accidents). **SliceModal-only tier priority is now `local → cloud → standard`** (was `cloud → local → standard`): imported profiles win because they carry parsed type/colour metadata in the response, while cloud entries don't (the per-preset detail endpoint rate-limits at ~10/sec per token and 50+ parallel fetches returned 429 on every request). The unified-listing endpoint's dedup pass now backfills metadata cross-tier — if a cloud entry wins dedup over a same-named local entry, the cloud entry inherits the local's `filament_type` / `filament_colour` so the Slice modal's metadata-aware pre-pick keeps working for users who have presets both cloud-synced and locally imported. Other consumers of `/slicer/presets` (Profiles page, etc.) retain the existing cloud-first dedup. **Sidecar** (orca-slicer-api fork, `bambuddy/profile-resolver` branch): `/slice` now accepts up to 16 repeated `filamentProfile` parts (was hard-capped at 1), the slicing service materializes each as `filament_N.json` and joins paths into a single `--load-filaments "a.json;b.json;c.json"` invocation; `/profiles/bundled` listing was extended with `filament_type` and `filament_colour` per leaf so the bundled tier carries metadata into the modal. **Sliced-archive card now reflects the actually-used filament list, not the project-wide AMS config**: `slice_and_persist_as_archive` previously copied `filament_type` and `filament_color` from the unsliced source archive verbatim, which inherited every project-wide AMS slot (16+ swatches on the card for a 2-color print). The new archive now reads those fields from the sliced output's `slice_info.config` via `ThreeMFParser` (which already gates on `used_g > 0`), falling back to the source archive's values only if parsing failed. **Backwards compatibility**: `SliceRequest` schema accepts three shapes — legacy `filament_preset_id: int`, source-aware singular `filament_preset: PresetRef`, multi-color array `filament_presets: list[PresetRef]` — the validator promotes any of them into a populated `filament_presets` list before the route handler runs, and stale browser tabs from before this change keep working unchanged. **Permissions**: no new endpoint paths added; the preview-slice runs inside `/filament-requirements` (gated on `LIBRARY_READ` / `ARCHIVES_READ`) and the multi-filament dispatch runs inside `POST /slice` (gated on `LIBRARY_UPLOAD`) — no auth surface widened. **Tests**: 6 schema tests for `SliceRequest` covering the multi-filament list shape and legacy-vs-new precedence; 9 unit tests for `slice_preview` covering happy path, content-hash invalidation, sidecar-failure no-cache-poison, concurrent-call thundering-herd guard via per-key `asyncio.Lock`, and LRU eviction-with-lock-cleanup; 15 unit tests for `extract_project_filaments_from_3mf` (5 cases) and `extract_plate_extruder_set_from_3mf` (10 cases including the 60/40 painted-threshold pin); a multi-filament wire-format test on `slice_with_profiles` pinning that N filament profiles produce N repeated multipart parts in submission order; 22 frontend SliceModal tests covering the plate picker step, multi-color rendering, metadata-aware pre-pick, manual slot override, archive-vs-library routing, and the new tier order. Localised across all 8 UI languages (English + German fully translated, the six others seeded with English copies pending native translation per the project's existing flow). - **Slicer presets now span Cloud, imported, and slicer-bundled tiers, end-to-end** — Initial slicer integration only saw DB-backed local imports, so a user without imported profiles got an empty Slice modal even when their Bambu Cloud account or the slicer sidecar carried perfectly usable presets. The Slice modal now pulls from three tiers in priority order — **cloud** (the user's own Bambu Cloud presets), **local** (DB-backed imports), **standard** (slicer-bundled stock profiles) — with name-based dedup so a preset that exists in multiple tiers only renders in the highest-priority one (cloud > local > standard) and within-tier order is preserved exactly. **Listing** (`GET /api/v1/slicer/presets`): cloud branch is per-user with a 5-minute cache keyed on `(user_id, sha256(token)[:16])` so a logout/login or token rotation auto-invalidates without callback wiring from the cloud-auth routes. Bundled branch is global with a 1-hour cache (sidecar's read-only filesystem only changes across image rebuilds). `cloud_status` (`ok` / `not_authenticated` / `expired` / `unreachable`) drives a precise modal banner instead of an unexplained empty list. **Slicing** (`POST /library/files/{id}/slice`, `POST /archives/{id}/slice`): request body now accepts source-aware `{source, id}` triplets per slot (cloud / local / standard) alongside the legacy `*_preset_id` fields for full backwards-compatibility — the schema validator normalises bare integer ids into `PresetRef(source='local', id=str(int))` so the dispatcher only deals with one shape. New `preset_resolver` service fetches the preset content per source: cloud via `BambuCloudService.get_setting_detail` (unwraps the `setting` envelope, falls back to top-level on minor shape variants), local from the DB (existing path), standard via a minimal `{inherits: , from: "system"}` stub that the sidecar's `bambuddy/profile-resolver` branch flattens against `BUNDLED_PROFILES_PATH//.json` — no preset-content round-trip needed for the standard tier. **Permissions**: the listing route gate matches the slice action itself (`LIBRARY_UPLOAD`) so any user who can slice can populate the dropdowns; the cloud branch has an independent `CLOUD_AUTH` check inside the fetch helper — a user holding `LIBRARY_UPLOAD` but not `CLOUD_AUTH` doesn't see the cloud tier (and can't slice with a cloud preset, returns 403) even if a leftover `User.cloud_token` survived a permission revocation. **SliceModal** (frontend): grouped `` per tier with localised section headers, default-selection follows the cloud > local > standard priority on first load, cloud-status banner with three variants (sign-in / expired / unreachable) only when the status isn't `ok`. **Sidecar** (orca-slicer-api fork, `bambuddy/profile-resolver` branch): new `GET /profiles/bundled` walks `BUNDLED_PROFILES_PATH/{machine,process,filament}` and returns instantiable presets only (`instantiation: "true"`), filtering out abstract bases like `fdm_filament_pla` so the dropdowns only offer things a user can actually pick. **Tests**: 17 unit tests for the listing endpoint helpers (dedup priority + per-slot scoping + order preservation, all four `cloud_status` states, `CLOUD_AUTH` defence-in-depth with token lookup short-circuit, per-user cache isolation, token-change cache invalidation, sidecar-unreachable fallback), 11 unit tests for the source-aware resolver (standard inherits-stub shape, local DB lookup with `preset_type` validation, cloud envelope unwrapping with both standard and top-level shapes, cloud auth-error → 401, cloud `CLOUD_AUTH` defence, slot dispatch routing), 6 schema tests for `SliceRequest` covering legacy bare-int normalisation and new source-aware refs and explicit-ref-wins-over-legacy precedence, 12 frontend tests for SliceModal covering tier-priority auto-selection, `` grouping, fallback when higher tiers are empty, source-aware payload on submit, manual override across tiers, archive-vs-library routing, error display, and all three banner variants. All 3391 backend + 1531 frontend tests pass.