fix(slicer): wrong-printer slicing + sliced-archive filament list + per-instance MakerWorld compat

Five stacked slice-pipeline bugs that each made the modal's profile picker
  theatrical for 3MF inputs:

  (1) `_strip_3mf_embedded_settings` removed `model_settings.config` /
      `slice_info.config` / `cut_information.xml` along with
      `project_settings.config`. The CLI silently exited after
      "Initializing StaticPrintConfigs" — exit 0, no result.json — and
      Bambuddy masked the failure by re-running with embedded settings
      and the source's bound printer. Strip removed from the dispatch
      path entirely.

  (2) Standard-tier preset stubs lacked the `type` field, so the CLI
      rejected `--load-settings` with rc=-5 ("input preset file is
      invalid") and the same masking fallback fired. Added
      `_SLOT_TO_PROFILE_TYPE` so each stub carries the right
      machine/process/filament discriminator.

  (3) Sliced-archive cards listed every project-wide AMS slot (16+
      swatches for a 2-color print). `slice_and_persist_as_archive` now
      reads `filament_type` / `filament_color` from the sliced output's
      `slice_info.config` (which `ThreeMFParser` already gates on
      `used_g > 0`) instead of inheriting from the source archive.

  (4) SliceModal had no warning when the picked printer profile didn't
      match the source 3MF — the CLI rejects cross-printer slices
      (rc=-16) and fell back to embedded settings, producing wrong-printer
      g-code that errored at print dispatch. Plates response now exposes
      `source_printer_model`; the modal compares against the picked
      profile name and disables Slice + shows an inline warning on
      mismatch.

  (5) MakerWorld URL-paste resolver listed plate instances without
      showing which printer each was sliced for (`/instances/hits`
      omits compatibility info that lives on `design.instances[]
      .extention.modelInfo`). The resolve route now joins both payloads
      by instance ID and forwards `compatibility` + `otherCompatibility`
      onto each hit; the MakerWorld page renders "Sliced for {primary}"
      + "Also marked compatible: ..." per row.

  Tests: 6 unit tests for `extract_source_printer_model_from_3mf`, 1 for
  filtered filament metadata via ThreeMFParser, 2 for makerworld resolve
  compat-merge (happy path + missing modelInfo), 3 frontend SliceModal
  tests for the printer-mismatch warning + Slice-disabled gate. New i18n
  keys `slice.printerMismatch`, `makerworld.slicedFor`,
  `makerworld.alsoCompatible` across all 8 locales.
This commit is contained in:
maziggy
2026-04-28 13:58:56 +02:00
parent 6497eefe60
commit c1f69ee0cc
25 changed files with 688 additions and 127 deletions
+85 -30
View File
@@ -241,11 +241,38 @@ 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.
const sourcePrinterModel = platesQuery.data?.source_printer_model ?? null;
const printerProfileName = printerPreset
? presetsQuery.data?.[printerPreset.source].printer.find((p) => p.id === printerPreset.id)?.name
: null;
// Profile names follow `<model> <nozzle> 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 while the printer mismatch warning is
// visible: clicking it would silently fall back to embedded settings and
// produce a wrong-printer file, the exact UX bug the warning is here to
// prevent. Only re-enable when the user picks a matching profile (or
// cloud preset whose name we can't parse).
const isReady =
printerPreset != null &&
processPreset != null &&
filamentPresets.length > 0 &&
filamentPresets.every((r) => r != null);
filamentPresets.every((r) => r != null) &&
!printerMismatch;
const isEnqueuing = enqueueMutation.isPending;
// Step 1: plate picker for multi-plate 3MF sources. Cancelling closes the
@@ -301,7 +328,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
{/* Body */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{(platesQuery.isLoading || presetsQuery.isLoading || filamentReqsQuery.isLoading) && (
{/* Preset listing loader — printer/process dropdowns can't render
without it. Plate query reuses the same spinner since it's
also blocking. */}
{(platesQuery.isLoading || presetsQuery.isLoading) && (
<div className="flex items-center gap-2 text-bambu-gray text-sm">
<Loader2 className="w-4 h-4 animate-spin" />
{t('slice.loadingPresets', 'Loading presets…')}
@@ -336,37 +366,62 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
onChange={setProcessPreset}
disabled={isEnqueuing}
/>
{filamentSlots.map((slot, idx) => (
<PresetDropdown
key={`filament-${idx}`}
label={
filamentSlots.length > 1
? t('slice.filamentSlot', {
index: idx + 1,
type: slot.type,
defaultValue: `Filament ${idx + 1} (${slot.type || ''})`,
})
: t('slice.filament', 'Filament profile')
}
slot="filament"
data={presetsQuery.data}
value={filamentPresets[idx] ?? null}
onChange={(ref) =>
setFilamentPresets((current) => {
const next = current.length === filamentSlots.length
? [...current]
: filamentSlots.map((_, i) => current[i] ?? null);
next[idx] = ref;
return next;
})
}
disabled={isEnqueuing}
swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
/>
))}
{/* Filament reqs may need a server-side preview-slice for
unsliced project files (single-pass, then cached). Show a
scoped spinner so the user sees the printer/process
dropdowns instead of an opaque "Loading presets…" wait. */}
{filamentReqsQuery.isLoading ? (
<div className="flex items-center gap-2 text-bambu-gray text-sm py-2">
<Loader2 className="w-4 h-4 animate-spin" />
{t('slice.analyzingPlateFilaments', 'Analyzing plate filaments…')}
</div>
) : (
filamentSlots.map((slot, idx) => (
<PresetDropdown
key={`filament-${idx}`}
label={
filamentSlots.length > 1
? t('slice.filamentSlot', {
index: idx + 1,
type: slot.type,
defaultValue: `Filament ${idx + 1} (${slot.type || ''})`,
})
: t('slice.filament', 'Filament profile')
}
slot="filament"
data={presetsQuery.data}
value={filamentPresets[idx] ?? null}
onChange={(ref) =>
setFilamentPresets((current) => {
const next = current.length === filamentSlots.length
? [...current]
: filamentSlots.map((_, i) => current[i] ?? null);
next[idx] = ref;
return next;
})
}
disabled={isEnqueuing}
swatchColor={filamentSlots.length > 1 ? slot.color : undefined}
/>
))
)}
</>
)}
{printerMismatch && (
<div
className="text-sm text-amber-200 bg-amber-900/20 border border-amber-700/40 rounded p-2"
role="alert"
>
{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.',
})}
</div>
)}
{errorMessage && (
<div className="text-sm text-red-400 bg-red-900/20 border border-red-900/40 rounded p-2" role="alert">
{errorMessage}