From ed27b27adbad0cb8c0c8fa3c4679a346de5d9c9f Mon Sep 17 00:00:00 2001 From: maziggy Date: Wed, 20 May 2026 12:21:33 +0200 Subject: [PATCH] =?UTF-8?q?=20=20feat(slice):=20cross-printer=20re-slicing?= =?UTF-8?q?=20=E2=80=94=20drop=20the=20gate,=20the=20banner,=20and=20the?= =?UTF-8?q?=20dead=20plumbing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 0 empirical test on 2026-05-20 disproved the "CLI cannot re-slice a 3MF for a different printer" assumption: feeding an 18-color H2D-bound Trent900.3mf to the X1C bundle via /slice produces valid X1C G-code in 1.8s, with bed (256x256), kinematics, nozzle count, machine_start_gcode, and bed_exclude_area all coming from the target bundle. - SliceModal: drop !printerMismatch from isReady; remove the banner and the sourcePrinterModel / printerProfileName / printerMismatch state entirely. Cross-printer slicing is now indistinguishable from a normal slice; the picker already shows the target printer. - Remove slice.printerMismatch from all 8 locales. - API cleanup: drop source_printer_model from /library/files/{id}/plates and /archives/{id}/plates responses, drop the field from frontend/src/types/plates.ts (PlateMetadata + LibraryFilePlatesResponse), delete extract_source_printer_model_from_3mf from threemf_tools.py and its 6 unit tests. Zero remaining consumers. - i18n discipline cleanup in SliceModal.tsx (same drop): strip every inline English defaultValue / positional fallback from t() calls (22 sites). Add slice.bundle / slice.bundleNone / slice.bundleAllRequired to all 8 locales — they had no entry in any locale file and were being served from the inline English fallback for every non-English user. - Tests: rewrite the mismatch-warning test to assert "no banner, Slice enabled" when models differ (regression guard); delete 2 obsolete tests covering gate states that no longer exist. --- CHANGELOG.md | 3 + backend/app/api/routes/archives.py | 9 -- backend/app/api/routes/library.py | 12 -- backend/app/utils/threemf_tools.py | 32 ----- backend/tests/unit/test_threemf_tools.py | 41 ------ .../__tests__/components/SliceModal.test.tsx | 95 +------------- frontend/src/components/SliceModal.tsx | 113 +++++----------- frontend/src/i18n/locales/de.ts | 4 +- frontend/src/i18n/locales/en.ts | 4 +- frontend/src/i18n/locales/fr.ts | 4 +- frontend/src/i18n/locales/it.ts | 4 +- frontend/src/i18n/locales/ja.ts | 4 +- frontend/src/i18n/locales/pt-BR.ts | 4 +- frontend/src/i18n/locales/zh-CN.ts | 4 +- frontend/src/i18n/locales/zh-TW.ts | 4 +- frontend/src/types/plates.ts | 6 - .../{index-BIzFPmFB.js => index-CHKJFwmW.js} | 122 +++++++++--------- static/index.html | 2 +- 18 files changed, 127 insertions(+), 340 deletions(-) rename static/assets/{index-BIzFPmFB.js => index-CHKJFwmW.js} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d48159e..109629f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to Bambuddy will be documented in this file. ### Added - **Currency: Belize Dollars (BZD) added to the Settings → Cost currency dropdown (#1454, requested by @PLGuerraDesigns)** — Reporter accurately tracks 3D-printing filament costs in his local currency and BZD wasn't selectable, forcing a manual 2:1 mental conversion from USD. Added `BZD: 'BZ$'` to `frontend/src/utils/currency.ts` next to MXN (Americas dollar-prefix grouping); `getCurrencySymbol('BZD')` returns `'BZ$'` and the SUPPORTED_CURRENCIES list now has 30 entries. Unit test added in `frontend/src/__tests__/utils/currency.test.ts` covering the symbol lookup and presence in SUPPORTED_CURRENCIES; entry-count assertion bumped to 30 so any future additions/removals are caught immediately. 14 currency tests green; frontend build clean. +### Changed +- **Slice modal: cross-printer 3MFs now re-slice transparently, banner removed, modal fully i18n'd** — Previous behaviour disabled the Slice button whenever the source 3MF's bound printer model didn't match the user's picked printer profile, on the theory that the slicer CLI "cannot re-slice a 3MF for a different printer" and would silently fall back to embedded settings to produce a wrong-printer file. Step 0 empirical test on 2026-05-20 disproved that: an 18-color H2D-bound `Trent900.3mf` sliced via the X1C bundle (`POST /slice` with `bundle=cb…X1C, printerName=# Bambu Lab X1 Carbon 0.4 nozzle`) produced 2.3 MB of genuinely X1C-compatible G-code in 1.8 s — `printer_model` overridden to `Bambu Lab X1 Carbon`, `printable_area` to 256×256 (X1C bed, not H2D's 350×320), `printable_height` 250 (vs 325), `bed_exclude_area` populated with X1C's 18×28 corner zone, `nozzle_diameter` single 0.4 (vs H2D's dual `0.4,0.4`), and the full X1C `machine_start_gcode` sequence baked in. The sidecar takes printer / process / first-N filament names from the picked bundle and only inherits embedded values for unused trailing slots — bed size, kinematics, start sequence all come from the target. **Behavioural change**: dropped `!printerMismatch` from the SliceModal `isReady` predicate so the Slice button stays enabled when models differ. The amber banner was first softened to an info message, then removed entirely — re-slicing across printers is now just a normal slice, the picker UI already shows which printer was picked, no second confirmation needed. **Dead-code removal (same drop)**: with no banner, the `source_printer_model` field on the `/library/files/{id}/plates` and `/archives/{id}/plates` responses had zero consumers; the `extract_source_printer_model_from_3mf` helper in `threemf_tools.py` (which opened the 3MF zip and read `Metadata/project_settings.config` on every plate request) had zero callers. Removed both response keys, both backend extractions, both `threemf_tools` imports, the helper itself, its 6 unit tests, the `source_printer_model` field from `frontend/src/types/plates.ts` (PlateMetadata + LibraryFilePlatesResponse), and 2 obsolete SliceModal tests that exercised the now-impossible matched-printer / legacy-archive paths. **i18n discipline cleanup (same drop, per [[feedback_no_followups]] + [[feedback_translate_dont_fallback]])**: every t() callsite in SliceModal.tsx had an inline English `defaultValue:` or positional-second-arg English fallback — 22 sites in total. With 8 locales shipped, those fallbacks are dead weight at best, and an actual i18n-violation when the key is missing because non-English users would silently see English. Audit found 3 keys (`slice.bundle`, `slice.bundleNone`, `slice.bundleAllRequired`) that had **no** corresponding entry in any locale file — they were being served from the inline English fallback exclusively, meaning every non-English user was already seeing those three labels in English. Added all 3 to all 8 locales with real translations, then stripped the English fallback from every t() call in SliceModal.tsx. The `slice.printerMismatch` key was removed from all 8 locales (banner is gone). **Why this matters**: a recurring pain point for users importing MakerWorld project files where the original creator's printer often differs from the user's; previously they had to round-trip through BambuStudio's "convert project" flow to re-export. Now Bambuddy re-slices in-place with no UI friction. **Tests**: the existing SliceModal "shows mismatch warning AND disables Slice" test was rewritten to assert "does not surface any cross-printer banner AND keeps Slice enabled when models differ" (regression guard against the gate being re-added); 2 obsolete tests deleted. 32 SliceModal tests green (was 34, -2 dead tests); 49 threemf_tools tests green (was 55, -6 helper tests); 24 plates-route tests green; frontend build clean; backend ruff clean; i18n parity check passes 4858 keys × 8 locales (net +2 vs pre-fix: +3 bundle keys, -1 printerMismatch). + ### Fixed - **Scheduler: queue items with `force_color_match` filament overrides now produce a correct AMS mapping at dispatch (#1437, fixed by external PR #1440 from @Person2099)** — Contributor's own bug report and fix. He had a queue item with `filament_overrides: [{slot_id: 1, type: "PLA", color: "#CBC6B8", force_color_match: true}]` and `ams_mapping: null`, expecting Bambuddy to translate the override into a slot mapping at dispatch time. Instead the scheduler dispatched with `ams_mapping: null` and the P1S fell back to type-only AMS matching, picking the wrong-colour slot. **Two-layer root cause** he traced end-to-end. **(1) `backend/app/services/filament_requirements.py:69`**: `extract_filament_requirements(file_path, plate_id=None)` fell through to `_collect_filaments(root, filaments)` whose XPath `./filament` only matches direct children of ``. Modern BambuStudio 3MFs wrap filaments inside `` elements, so this XPath returned `[]` on every modern multi-plate 3MF when no specific plate was targeted — which is the standard scheduler call shape for queue items without a pinned plate. The downstream "no AMS mapping" cascade ALL flowed from this empty filament_reqs result. Fix walks `` elements first, dedupes by `slot_id` (highest `used_grams` wins on ties — sane because BambuStudio slots are project-wide and the entry that extruded the most is the most representative for AMS planning), and preserves the old `./filament` XPath as a fallback when no `` elements are present, so legacy 3MFs continue to parse unchanged. **(2) `backend/app/services/print_scheduler.py:792` — defence in depth**: even with (1) in place, edge cases exist where `_get_filament_requirements` can still return None (3MF missing `slice_info.config` entirely, IO failure during ZIP extraction, etc). New `_build_override_direct_mapping(force_overrides, status)` helper kicks in at exactly that moment when `force_color_match` overrides are present — builds the requirement list directly from the overrides (`slot_id`, `type`, `color`, empty `tray_info_idx`) and delegates to the existing `_match_filaments_to_slots()` cascade against the printer's loaded AMS state. Wrong-colour slot credit via the cascade's type-only fallback is impossible-by-construction because the upstream `_get_missing_force_color_slots()` printer-eligibility gate at `:590` already requires an exact `(type, normalised colour)` pair to be loaded *before* the printer is even considered for the job, so by the time `_build_override_direct_mapping` runs the exact match is guaranteed in the loaded set and the cascade's `exact_match` branch wins (colour normalisation is identical on both sides — `tray_color.replace("#", "").lower()[:6]`). Pref-only overrides (without `force_color_match`) intentionally do NOT trigger the fallback — they keep the pre-PR "no mapping, printer picks defaults" behaviour, so the new fallback is strictly opt-in via `force_color_match: true`. **Backwards-compat triple-checked**: legacy 3MF format unchanged (preserved fallback path); `plate_id != None` branch untouched (entire fix is inside the `else` of `if plate_id is not None`); `filament_overrides=None` / `[]` / no-force-entries all preserve the existing `return None` path; malformed JSON in `filament_overrides` is caught by the existing try/except, logged, and still returns None. **Tests** (22 new across two files; all pass on `pytest -n 30`): `backend/tests/unit/services/test_filament_requirements.py` — 4 tests covering the `plate_id=None` modern-format path, multi-plate collection, slot-dedup-by-highest-grams, and single-plate-modern-format. `backend/tests/unit/test_scheduler_force_color_ams_fallback.py` — 18 tests across `TestBuildOverrideDirectMapping` (single override matches AMS slot, empty AMS returns None, no colour match still produces a mapping length, multi-override produces multi-element mapping, external spool match yields global_tray_id 254, `tray_info_idx` is cleared) and `TestComputeAmsMappingFallback` (fallback used when reqs empty + force overrides present, fallback NOT used when no force_color flag, fallback NOT used when overrides None, normal path still used when reqs available, printer-status-unavailable returns None gracefully). 5079 backend tests + ruff + frontend build all clean post-merge; #1457/#1459/#1440 verified non-interacting (different services, different code paths, different timings). External-PR-checklist (per [[feedback_pr_changelog_required]]): contributor doesn't add CHANGELOG, this entry added by Martin post-merge. diff --git a/backend/app/api/routes/archives.py b/backend/app/api/routes/archives.py index 227afd4b7..ef4a70704 100644 --- a/backend/app/api/routes/archives.py +++ b/backend/app/api/routes/archives.py @@ -33,7 +33,6 @@ from backend.app.utils.http import build_content_disposition from backend.app.utils.threemf_tools import ( extract_nozzle_mapping_from_3mf, extract_project_filaments_from_3mf, - extract_source_printer_model_from_3mf, ) logger = logging.getLogger(__name__) @@ -3316,20 +3315,12 @@ async def get_archive_plates( # to preview gcode — the viewer, skip-objects — can gate on this instead of # 404-ing on every plate request. has_gcode = bool(gcode_files) - # SliceModal pre-check signal — see library.py for rationale. - source_printer_model: str | None = None - try: - with zipfile.ZipFile(file_path, "r") as zf: - source_printer_model = extract_source_printer_model_from_3mf(zf) - except (zipfile.BadZipFile, OSError): - pass return { "archive_id": archive_id, "filename": archive.filename, "plates": plates, "is_multi_plate": len(plates) > 1, "has_gcode": has_gcode, - "source_printer_model": source_printer_model, } diff --git a/backend/app/api/routes/library.py b/backend/app/api/routes/library.py index c7908d3b5..6994641a2 100644 --- a/backend/app/api/routes/library.py +++ b/backend/app/api/routes/library.py @@ -67,7 +67,6 @@ from backend.app.services.stl_thumbnail import generate_stl_thumbnail from backend.app.utils.threemf_tools import ( extract_nozzle_mapping_from_3mf, extract_project_filaments_from_3mf, - extract_source_printer_model_from_3mf, ) logger = logging.getLogger(__name__) @@ -2419,22 +2418,11 @@ async def get_library_file_plates( except Exception as e: logger.warning("Failed to parse plates from library file %s: %s", file_id, e) - # SliceModal pre-check signal: the source 3MF's bound printer model. The - # CLI cannot re-slice for a different printer; surface this so the modal - # can warn the user before they pick a mismatched profile. - source_printer_model: str | None = None - try: - with zipfile.ZipFile(file_path, "r") as zf: - source_printer_model = extract_source_printer_model_from_3mf(zf) - except (zipfile.BadZipFile, OSError): - pass - return { "file_id": file_id, "filename": lib_file.filename, "plates": plates, "is_multi_plate": len(plates) > 1, - "source_printer_model": source_printer_model, } diff --git a/backend/app/utils/threemf_tools.py b/backend/app/utils/threemf_tools.py index ff26bf176..bcaf4e240 100644 --- a/backend/app/utils/threemf_tools.py +++ b/backend/app/utils/threemf_tools.py @@ -609,38 +609,6 @@ def inject_gcode_into_3mf( return None -def extract_source_printer_model_from_3mf(zf: zipfile.ZipFile) -> str | None: - """Source 3MF's bound printer model from ``Metadata/project_settings.config``. - - Returns e.g. ``"Bambu Lab A1"`` when the project was built for an A1, or - ``None`` when the file lacks the metadata or the field is absent. The - SliceModal uses this to warn the user before slicing if the chosen - printer profile targets a different model — the slicer CLI rejects - cross-printer slicing with rc=-16 and the result, when the strip + load - fallback masks it, is a misleadingly-tagged archive. - """ - if "Metadata/project_settings.config" not in zf.namelist(): - return None - try: - proj = json.loads(zf.read("Metadata/project_settings.config").decode()) - except (ValueError, OSError): - return None - if not isinstance(proj, dict): - return None - model = proj.get("printer_model") - if isinstance(model, str) and model.strip(): - return model.strip() - # Some older Bambu Studio exports stored the model under - # ``printer_settings_id`` (e.g. "Bambu Lab A1 0.4 nozzle"); strip the - # nozzle suffix to get the canonical model name. Best-effort — if the - # field doesn't follow the convention we leave it as-is. - settings_id = proj.get("printer_settings_id") - if isinstance(settings_id, str) and settings_id.strip(): - # Drop trailing " 0.4 nozzle" / " 0.2 nozzle" / etc. - return re.sub(r"\s+0\.\d+\s+nozzle$", "", settings_id.strip()) - return None - - def extract_project_filaments_from_3mf(zf: zipfile.ZipFile) -> list[dict]: """Project-wide AMS slot config from ``Metadata/project_settings.config``. diff --git a/backend/tests/unit/test_threemf_tools.py b/backend/tests/unit/test_threemf_tools.py index 60376af20..7ec4688d8 100644 --- a/backend/tests/unit/test_threemf_tools.py +++ b/backend/tests/unit/test_threemf_tools.py @@ -13,7 +13,6 @@ from backend.app.utils.threemf_tools import ( extract_filament_usage_from_3mf, extract_plate_extruder_set_from_3mf, extract_project_filaments_from_3mf, - extract_source_printer_model_from_3mf, get_cumulative_usage_at_layer, mm_to_grams, parse_gcode_layer_filament_usage, @@ -646,43 +645,3 @@ class TestExtractPlateExtruderSetFrom3mf: # Top-level metadata still works; missing component model file # is silently skipped without crashing. assert extract_plate_extruder_set_from_3mf(zf, plate_id=1) == {2} - - -# --------------------------------------------------------------------------- -# Tests for extract_source_printer_model_from_3mf — feeds the SliceModal's -# pre-slice mismatch warning. The CLI cannot re-slice a 3MF for a different -# printer, so warning the user up-front avoids producing wrong-printer output. -# --------------------------------------------------------------------------- - - -class TestExtractSourcePrinterModelFrom3mf: - def test_returns_none_when_project_settings_missing(self): - with _make_3mf_with({"placeholder.txt": "hi"}) as zf: - assert extract_source_printer_model_from_3mf(zf) is None - - def test_reads_printer_model_directly(self): - proj = {"printer_model": "Bambu Lab H2D"} - with _make_3mf_with({"Metadata/project_settings.config": json.dumps(proj)}) as zf: - assert extract_source_printer_model_from_3mf(zf) == "Bambu Lab H2D" - - def test_falls_back_to_printer_settings_id_with_nozzle_strip(self): - # Older Bambu Studio exports stored the printer under - # printer_settings_id, often with a "0.4 nozzle" suffix the helper - # must strip to match the canonical model name. - proj = {"printer_settings_id": "Bambu Lab A1 0.4 nozzle"} - with _make_3mf_with({"Metadata/project_settings.config": json.dumps(proj)}) as zf: - assert extract_source_printer_model_from_3mf(zf) == "Bambu Lab A1" - - def test_settings_id_without_nozzle_suffix_returned_as_is(self): - proj = {"printer_settings_id": "Bambu Lab P1S"} - with _make_3mf_with({"Metadata/project_settings.config": json.dumps(proj)}) as zf: - assert extract_source_printer_model_from_3mf(zf) == "Bambu Lab P1S" - - def test_corrupt_json_returns_none_no_exception(self): - with _make_3mf_with({"Metadata/project_settings.config": b"{broken"}) as zf: - assert extract_source_printer_model_from_3mf(zf) is None - - def test_empty_string_treated_as_missing(self): - proj = {"printer_model": "", "printer_settings_id": ""} - with _make_3mf_with({"Metadata/project_settings.config": json.dumps(proj)}) as zf: - assert extract_source_printer_model_from_3mf(zf) is None diff --git a/frontend/src/__tests__/components/SliceModal.test.tsx b/frontend/src/__tests__/components/SliceModal.test.tsx index 94c873eb7..07809c3a2 100644 --- a/frontend/src/__tests__/components/SliceModal.test.tsx +++ b/frontend/src/__tests__/components/SliceModal.test.tsx @@ -764,18 +764,16 @@ describe('SliceModal', () => { }); }); - // Pre-slice printer-mismatch warning. The slicer CLI cannot re-slice a - // 3MF for a different printer model — clicking Slice in that state - // would silently fall back to the embedded settings and produce a - // wrong-printer file. The modal surfaces a warning and disables Slice - // when the source's source_printer_model doesn't match the picked - // printer profile. - it('shows a printer-mismatch warning and disables Slice when models differ', async () => { + // Cross-printer re-slicing is a normal, supported operation as of + // 2026-05-20 (Step 0 empirical test: sidecar overrides printer / process + // / bed / kinematics from the picked bundle, producing valid target- + // printer G-code). No banner, no warning — the picker UI already shows + // which printer the user picked, and that's enough. + it('does not surface any cross-printer banner and keeps Slice enabled when models differ', async () => { mockApi.getLibraryFilePlates.mockResolvedValue({ file_id: 100, filename: 'A1Original.3mf', is_multi_plate: false, - source_printer_model: 'A1', plates: [ { index: 1, @@ -807,86 +805,7 @@ describe('SliceModal', () => { expect(screen.getByText('Bambu Lab X1 Carbon 0.4 nozzle')).toBeDefined(), ); - // Warning banner is visible (role=alert) and references both models. - const alert = await screen.findByRole('alert'); - expect(alert.textContent).toMatch(/A1/); - expect(alert.textContent).toMatch(/X1 Carbon/); - - // Slice button is disabled while the warning is up. - const sliceButton = screen.getByRole('button', { name: /^Slice$/ }) as HTMLButtonElement; - expect(sliceButton.disabled).toBe(true); - }); - - it('keeps Slice enabled when the picked profile matches the source printer model', async () => { - mockApi.getLibraryFilePlates.mockResolvedValue({ - file_id: 100, - filename: 'X1COriginal.3mf', - is_multi_plate: false, - source_printer_model: 'X1 Carbon', - plates: [ - { - index: 1, - name: 'Plate 1', - objects: [], - has_thumbnail: false, - thumbnail_url: null, - print_time_seconds: null, - filament_used_grams: null, - filaments: [], - }, - ], - }); - mockApi.getSlicerPresets.mockResolvedValue(makeUnified({ - standard: { - printer: [{ id: 'Bambu Lab X1 Carbon 0.4 nozzle', name: 'Bambu Lab X1 Carbon 0.4 nozzle', source: 'standard' }], - process: [{ id: '0.20mm Standard', name: '0.20mm Standard', source: 'standard' }], - filament: [{ id: 'Bambu PLA Basic', name: 'Bambu PLA Basic', source: 'standard' }], - }, - })); - - renderWithTracker({ - source: { kind: 'libraryFile', id: 100, filename: 'X1COriginal.3mf' }, - onClose: vi.fn(), - }); - - await waitFor(() => - expect(screen.getByText('Bambu Lab X1 Carbon 0.4 nozzle')).toBeDefined(), - ); - - // No mismatch warning. - expect(screen.queryByRole('alert')).toBeNull(); - const sliceButton = screen.getByRole('button', { name: /^Slice$/ }) as HTMLButtonElement; - expect(sliceButton.disabled).toBe(false); - }); - - it('keeps Slice enabled when source_printer_model is unknown (legacy archives)', async () => { - // Older 3MFs without project_settings.printer_model fall through to - // no-warning — we don't have enough info to gate the user. - mockApi.getLibraryFilePlates.mockResolvedValue({ - file_id: 100, - filename: 'Legacy.3mf', - is_multi_plate: false, - source_printer_model: null, - plates: [ - { - index: 1, - name: 'Plate 1', - objects: [], - has_thumbnail: false, - thumbnail_url: null, - print_time_seconds: null, - filament_used_grams: null, - filaments: [], - }, - ], - }); - - renderWithTracker({ - source: { kind: 'libraryFile', id: 100, filename: 'Legacy.3mf' }, - onClose: vi.fn(), - }); - - await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined()); + // No banner, no alert — re-slicing across printers is just a normal slice now. expect(screen.queryByRole('alert')).toBeNull(); const sliceButton = screen.getByRole('button', { name: /^Slice$/ }) as HTMLButtonElement; expect(sliceButton.disabled).toBe(false); diff --git a/frontend/src/components/SliceModal.tsx b/frontend/src/components/SliceModal.tsx index 98f15587e..0946765e1 100644 --- a/frontend/src/components/SliceModal.tsx +++ b/frontend/src/components/SliceModal.tsx @@ -189,7 +189,7 @@ function FilamentAnalysisSpinner({ } else { showPersistentToast( toastId, - t('slice.previewToast', 'Analyzing {{name}} — {{elapsed}}', { + t('slice.previewToast', { name: prettyName, elapsed: elapsedStr, }), @@ -206,7 +206,7 @@ function FilamentAnalysisSpinner({ const inlineLabel = stage && typeof percent === 'number' && percent > 0 ? `${stage} (${Math.min(100, Math.max(0, Math.round(percent)))}%)` - : t('slice.analyzingPlateFilaments', 'Analyzing plate filaments…'); + : t('slice.analyzingPlateFilaments'); return (
@@ -425,7 +425,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) { bundleFilamentNames.length === 0 || bundleFilamentNames.some((n) => n == null) ) { - throw new Error(t('slice.bundleAllRequired', 'Bundle process and every filament slot must be picked')); + throw new Error(t('slice.bundleAllRequired')); } const bundleSpec: SliceBundleSpec = { bundle_id: selectedBundle.id, @@ -447,7 +447,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) { filamentPresets.length === 0 || filamentPresets.some((r) => r == null) ) { - throw new Error(t('slice.allPresetsRequired', 'All presets must be selected')); + throw new Error(t('slice.allPresetsRequired')); } body = { printer_preset: printerPreset, @@ -480,55 +480,20 @@ export function SliceModal({ source, onClose }: SliceModalProps) { }, }); - // Pre-slice compatibility check: the slicer CLI (both OrcaSlicer and - // BambuStudio) cannot re-slice a 3MF for a printer different from the one - // it was originally bound to — the cross-printer "convert project" flow - // is desktop-Studio only. If we can match the source's printer model to a - // SliceModal-known model and the user's chosen printer profile names a - // different model, surface a warning before they click Slice. - // - // For bundle mode, the bundle's printer_preset_name plays the same role - // as the picked PresetRef's resolved name in preset mode. - const sourcePrinterModel = platesQuery.data?.source_printer_model ?? null; - const printerProfileName = isBundleMode - ? selectedBundle?.printer_preset_name.replace(/^# /, '') ?? null - : printerPreset - ? presetsQuery.data?.[printerPreset.source].printer.find((p) => p.id === printerPreset.id)?.name - : null; - // Profile names follow ` nozzle` (e.g. "Bambu Lab H2D 0.4 - // nozzle"). The CLI compat check uses the model prefix; substring match - // catches both standard and locally-imported user-named profiles that - // include the model in the name. Cloud presets with arbitrary names - // (e.g. "My Custom X1C") fall through to no-warning, which is a - // reasonable default — the user picked it knowingly. - const printerMismatch = - !!sourcePrinterModel && - !!printerProfileName && - !printerProfileName.toLowerCase().includes(sourcePrinterModel.toLowerCase()); - - // Slice button stays disabled until *all* of these hold: - // - the preview slice / embedded-metadata read has succeeded so we know - // the per-plate filament slot list is final - // (filamentReqsQuery.isSuccess). Without this gate the synthetic - // single-slot fallback would auto-enable the button on opaque - // defaults, before the slicer has even returned the real slot map. - // - printer + process picked, every filament slot has a profile (the - // auto-pick fills these once filamentSlots arrives) - // - no printer-mismatch warning is up (clicking would silently fall - // back to embedded settings and produce a wrong-printer file) + // Slice button stays disabled until the preview slice / embedded-metadata + // read has succeeded (filamentReqsQuery.isSuccess) and every filament slot + // has a picked profile. const isReady = isBundleMode ? selectedBundle != null && bundleProcessName != null && filamentReqsQuery.isSuccess && bundleFilamentNames.length > 0 && - bundleFilamentNames.every((n) => n != null) && - !printerMismatch + bundleFilamentNames.every((n) => n != null) : printerPreset != null && processPreset != null && filamentReqsQuery.isSuccess && filamentPresets.length > 0 && - filamentPresets.every((r) => r != null) && - !printerMismatch; + filamentPresets.every((r) => r != null); const isEnqueuing = enqueueMutation.isPending; // Step 1: plate picker for multi-plate 3MF sources. Cancelling closes the @@ -563,7 +528,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) {

- {t('slice.title', 'Slice model')} + {t('slice.title')}

{source.filename} @@ -576,7 +541,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) { onClick={onClose} disabled={isEnqueuing} className="flex-shrink-0 text-bambu-gray hover:text-white transition-colors disabled:opacity-50" - aria-label={t('common.close', 'Close')} + aria-label={t('common.close')} > @@ -590,7 +555,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) { {(platesQuery.isLoading || presetsQuery.isLoading) && (

- {t('slice.loadingPresets', 'Loading presets…')} + {t('slice.loadingPresets')}
)} @@ -624,7 +589,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) { {!isBundleMode && ( <>
{selectedBundle.printer_preset_name}
)} - {printerMismatch && ( -
- {t('slice.printerMismatch', { - source: sourcePrinterModel, - target: printerProfileName, - defaultValue: - 'This 3MF was sliced for {{source}}, but you picked {{target}}. The slicer CLI cannot re-slice a 3MF for a different printer — open the source in Bambu Studio, change the printer, and re-export.', - })} -
- )} - {errorMessage && (
{errorMessage} @@ -792,7 +741,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) { disabled={isEnqueuing} className="px-3 py-1.5 text-sm rounded-md border border-bambu-dark-tertiary text-bambu-gray hover:text-white hover:border-bambu-gray transition-colors disabled:opacity-50" > - {t('common.cancel', 'Cancel')} + {t('common.cancel')}
@@ -885,7 +834,7 @@ function BedTypeDropdown({ return (