mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 07:01:40 +02:00
Bundle import never delivered what it implied: BambuStudio's .bbscfg export
strips system processes/filaments, so importing a bundle left users without
process presets and slicing fell back to embedded settings on STL. Bundle
mode also hid the standard tier behind a constrained dropdown, the actual
trap reported here.
Removed end-to-end:
- backend: POST/GET/DELETE /slicer/bundles*, SliceRequest.bundle,
SliceBundleSpec, dispatch fork in library.py, bundle-context params on
the filament-requirements endpoints, bundle-fingerprint cache key in
slice_preview.py, SlicerApiService.{import,list,get,delete}_bundle and
slice_with_bundle, BundleSummary / BundleNotFoundError.
- frontend: BundlePicker + BundleStringDropdown, isBundleMode + every
branch, bundle state/queries/dispatch in SliceModal.tsx, SlicerBundle /
SliceBundleSpec types, three bundle API methods. buildCompatibilityIndex
loses its bundle path; presetCompatibility keeps compatible_printers
plus the @BBL fallback.
- SlicerBundlesPanel turns into a permanent static notice explaining the
removal, alternative import paths, and the new slice-time lookup order
(Imported > Orca Cloud > Bambu Cloud > Standard sidecar fallback).
- i18n: slicerBundlesRemoved.{title,description,alternatives,lookupOrder}
translated across all 11 locales; slice.bundle*, slicerBundles.* keys
removed.
Fixed (surfaced by removing bundle mode):
- _resolve_cloud and _resolve_orca_cloud now force type per slot and pin
from: "system" on the payload before json.dumps. Bambu Cloud ships
type as "printer"/"print" and routinely empty `from`; the BS CLI's
--load-settings parser rejects both with return -5 / "input preset
file invalid". Standard tier already did this; cloud paths now match.
214 lines
9.5 KiB
TypeScript
214 lines
9.5 KiB
TypeScript
// Printer-compatibility matching for the SliceModal's process / filament
|
|
// dropdowns (#1325).
|
|
//
|
|
// Compatibility is resolved in this order, stopping on the first non-unknown
|
|
// answer:
|
|
//
|
|
// 1. Imported (local-tier) presets carry the slicer's own
|
|
// `compatible_printers` list — an exact list of printer-preset names.
|
|
// 2. BambuStudio's own `@BBL <model>` naming convention on shipped cloud
|
|
// / standard presets. The token → printer-fragment table is derived
|
|
// from the backend's canonical PRINTER_MODEL_MAP (fetched via
|
|
// /slicer/printer-models), not duplicated here.
|
|
//
|
|
// The result drives grouping, not hard hiding: a preset no rule covers
|
|
// stays in the main list, and only a preset that resolves to a *different*
|
|
// printer is pushed into an "Other printers" group.
|
|
|
|
export type PrinterCompatibility = 'match' | 'mismatch' | 'unknown';
|
|
|
|
// Lookup tables consumed by `presetCompatibility`. `bambuModelByShortCode`
|
|
// is the @BBL token → printer-preset fragment map derived from the backend's
|
|
// PRINTER_MODEL_MAP — e.g. `X1C` → `X1 Carbon`. An empty map means the @BBL
|
|
// fallback still works when token and printer-name fragment match directly
|
|
// (raw-token comparison), and gracefully degrades otherwise.
|
|
export interface PrinterCompatibilityIndex {
|
|
bambuModelByShortCode: Record<string, string>;
|
|
}
|
|
|
|
/** An empty index — used when the model map hasn't loaded yet. */
|
|
export const EMPTY_COMPATIBILITY_INDEX: PrinterCompatibilityIndex = {
|
|
bambuModelByShortCode: {},
|
|
};
|
|
|
|
// Bambu cloud started shipping terse model codes in `@BBL <code>` suffixes
|
|
// mid-2026 — the most visible one is "A1 Mini" → "A1M" (#1649, reported by
|
|
// @technopaw). User-authored profiles still use the long display name, so
|
|
// both shapes have to match the same printer. The table is uppercase-normalised
|
|
// for case-insensitive lookups; add a row when a future rename is spotted via
|
|
// `/api/v1/cloud/settings`. Keep narrow on purpose — wide-net aliasing
|
|
// (e.g. "X1" ⇄ "X1C") would silently group truly distinct printers.
|
|
const PRINTER_MODEL_SUFFIX_ALIASES: Record<string, readonly string[]> = {
|
|
'A1 MINI': ['A1M'],
|
|
};
|
|
|
|
/**
|
|
* True when ``presetSuffix`` (the token extracted from a "@BBL <code>" or
|
|
* preset-name suffix) refers to the same printer as ``printerModel``
|
|
* (the display name selected in the picker). Case-insensitive; consults
|
|
* the alias table for short codes Bambu introduced after the long forms
|
|
* shipped (#1649).
|
|
*/
|
|
export function matchesPrinterModelSuffix(presetSuffix: string, printerModel: string): boolean {
|
|
const p = presetSuffix.toUpperCase();
|
|
const m = printerModel.toUpperCase();
|
|
if (p === m) return true;
|
|
const aliasesOfM = PRINTER_MODEL_SUFFIX_ALIASES[m];
|
|
if (aliasesOfM && aliasesOfM.includes(p)) return true;
|
|
const aliasesOfP = PRINTER_MODEL_SUFFIX_ALIASES[p];
|
|
if (aliasesOfP && aliasesOfP.includes(m)) return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Invert the backend's PRINTER_MODEL_MAP into the shape the @BBL fallback
|
|
* needs: short code → printer-preset fragment (the part of "Bambu Lab X1
|
|
* Carbon" the user sees in a printer preset name, minus the "Bambu Lab "
|
|
* brand prefix).
|
|
*
|
|
* Backend ships e.g. `{"Bambu Lab X1 Carbon": "X1C", "Bambu Lab A1 mini":
|
|
* "A1 Mini", "Bambu Lab A1 Mini": "A1 Mini"}` — multiple long forms can map
|
|
* to the same short. We pick the first long-form encountered for each short
|
|
* code; case normalisation happens at match time so "A1 mini" vs "A1 Mini"
|
|
* never matters.
|
|
*/
|
|
function buildShortCodeMap(
|
|
printerModels: Record<string, string>,
|
|
): Record<string, string> {
|
|
const out: Record<string, string> = {};
|
|
for (const [longName, shortCode] of Object.entries(printerModels)) {
|
|
if (shortCode in out) continue;
|
|
out[shortCode] = longName.replace(/^Bambu Lab\s+/, '');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Build the compatibility index from the backend printer-model registry.
|
|
*/
|
|
export function buildCompatibilityIndex(
|
|
printerModels: Record<string, string> = {},
|
|
): PrinterCompatibilityIndex {
|
|
return {
|
|
bambuModelByShortCode: buildShortCodeMap(printerModels),
|
|
};
|
|
}
|
|
|
|
function normalizeModelFragment(s: string): string {
|
|
return s.replace(/\s+/g, '').toLowerCase();
|
|
}
|
|
|
|
// Bambu Studio's naming convention for bundled presets: the 0.4 nozzle is
|
|
// the default and its variants drop the nozzle suffix; 0.2 / 0.6 / 0.8
|
|
// carry an explicit "<size> nozzle" segment. So a process with no suffix
|
|
// is implicitly a 0.4 process — required to compare correctly against a
|
|
// 0.4 printer preset, which DOES carry the suffix.
|
|
const DEFAULT_NOZZLE = '0.4';
|
|
|
|
// Strip a trailing "<size> nozzle" segment, returning the nozzle string
|
|
// (e.g. "0.6") or null when absent. Used by both BBL-token and printer-
|
|
// preset extractors so the suffix is parsed identically on both sides.
|
|
function takeNozzleSuffix(s: string): { stripped: string; nozzle: string | null } {
|
|
const m = s.match(/^(.*?)\s+([\d.]+)\s*nozzle\s*$/i);
|
|
if (!m) return { stripped: s.trim(), nozzle: null };
|
|
return { stripped: m[1].trim(), nozzle: m[2] };
|
|
}
|
|
|
|
// Pull the model token and nozzle out of a "@BBL <token> [<size> nozzle]"
|
|
// suffix. The token may contain a space (e.g. "A1 mini"), so we strip a
|
|
// trailing nozzle segment rather than splitting on the first whitespace.
|
|
function extractBblToken(presetName: string): { token: string; nozzle: string | null } | null {
|
|
const marker = '@BBL ';
|
|
const idx = presetName.indexOf(marker);
|
|
if (idx < 0) return null;
|
|
const rest = presetName.slice(idx + marker.length).trim();
|
|
const { stripped, nozzle } = takeNozzleSuffix(rest);
|
|
return stripped ? { token: stripped, nozzle } : null;
|
|
}
|
|
|
|
// Pull the model fragment and nozzle out of a "Bambu Lab <model> [<size>
|
|
// nozzle]" printer preset name. Returns null for non-Bambu printer
|
|
// presets — there is no reliable name-based match against those.
|
|
function extractPrinterPresetModel(printerPresetName: string): { model: string; nozzle: string | null } | null {
|
|
const m = printerPresetName.match(/^Bambu Lab\s+(.+)$/i);
|
|
if (!m) return null;
|
|
const { stripped, nozzle } = takeNozzleSuffix(m[1]);
|
|
return stripped ? { model: stripped, nozzle } : null;
|
|
}
|
|
|
|
/**
|
|
* Name-based fallback for presets BambuStudio ships with a `@BBL <model>`
|
|
* tag (#1325 follow-up). Used only after `compatible_printers` has returned
|
|
* `'unknown'`.
|
|
*
|
|
* Compares BOTH model AND nozzle. The nozzle filter is required because
|
|
* Bambu ships per-nozzle process / filament variants (0.2 / 0.4 / 0.6 /
|
|
* 0.8) — a 0.6-nozzle process is unusable on a 0.4-nozzle printer.
|
|
* 0.4 is Bambu's default and its variants drop the nozzle suffix, so a
|
|
* preset with no suffix counts as 0.4.
|
|
*/
|
|
function classifyByBambuName(
|
|
presetName: string,
|
|
selectedPrinterName: string,
|
|
bambuModelByShortCode: Record<string, string>,
|
|
): PrinterCompatibility {
|
|
const parsed = extractBblToken(presetName);
|
|
if (!parsed) return 'unknown';
|
|
// If the token isn't in the table (a brand-new Bambu model whose short
|
|
// code the backend registry hasn't added yet, or the model map hasn't
|
|
// loaded yet), fall back to comparing the raw token. That keeps the
|
|
// matcher working when token and printer-name fragment happen to be
|
|
// identical — e.g. "Q1" preset against "Bambu Lab Q1 0.4 nozzle" —
|
|
// without us having to ship a code update. When they differ in form
|
|
// (X1C vs "X1 Carbon"), the registry is what makes the match work.
|
|
const inferredModel = bambuModelByShortCode[parsed.token] ?? parsed.token;
|
|
const selectedParts = extractPrinterPresetModel(selectedPrinterName);
|
|
if (!selectedParts) return 'unknown';
|
|
// The raw inferred model and the printer-preset fragment may differ only by
|
|
// the Bambu short-code rename (e.g. preset token "A1M" vs printer "A1 Mini").
|
|
// ``matchesPrinterModelSuffix`` consults the alias table before declaring a
|
|
// mismatch — see #1649.
|
|
if (
|
|
normalizeModelFragment(selectedParts.model) !== normalizeModelFragment(inferredModel)
|
|
&& !matchesPrinterModelSuffix(parsed.token, selectedParts.model)
|
|
) {
|
|
return 'mismatch';
|
|
}
|
|
// Nozzle compare — only when we have a usable size from the printer
|
|
// side. A Bambu printer preset always carries one, so this branch is
|
|
// taken in practice; the null path is defensive degrade for hand-typed
|
|
// or non-Bambu printer names that happened to match the model.
|
|
if (selectedParts.nozzle !== null) {
|
|
const presetNozzle = parsed.nozzle ?? DEFAULT_NOZZLE;
|
|
if (presetNozzle !== selectedParts.nozzle) return 'mismatch';
|
|
}
|
|
return 'match';
|
|
}
|
|
|
|
/**
|
|
* Classify a process / filament preset against the selected printer.
|
|
*
|
|
* - 'match' — the preset is compatible with the selected printer.
|
|
* - 'mismatch' — the preset resolves to a *different* printer.
|
|
* - 'unknown' — compatibility can't be determined (no `compatible_printers`,
|
|
* no recognizable `@BBL` tag, or no printer is selected);
|
|
* the caller must not hide it.
|
|
*/
|
|
export function presetCompatibility(
|
|
preset: { name: string; compatible_printers?: string[] | null },
|
|
_slot: 'process' | 'filament',
|
|
selectedPrinterName: string | null,
|
|
index: PrinterCompatibilityIndex,
|
|
): PrinterCompatibility {
|
|
if (!selectedPrinterName) return 'unknown';
|
|
// (1) Imported presets carry the slicer's own compatible_printers list —
|
|
// authoritative when set.
|
|
const compat = preset.compatible_printers;
|
|
if (compat && compat.length > 0) {
|
|
return compat.includes(selectedPrinterName) ? 'match' : 'mismatch';
|
|
}
|
|
// (2) BambuStudio's `@BBL <model>` name convention — covers cloud /
|
|
// standard presets that don't carry compatible_printers.
|
|
return classifyByBambuName(preset.name, selectedPrinterName, index.bambuModelByShortCode);
|
|
}
|