mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
fix(presets): match Bambu cloud @BBL A1M as A1 Mini (#1649)
Reporter on an A1 Mini saw the AMS slot Configure dropdown render no
Bambu / Generic filament profiles, and saw the Profiles tab strip
A1 Mini results when filtering by that model. Bambu rolled out a
profile rename mid-2026: the @BBL <code> suffix on 106 cloud profiles
shifted from the long display form to a terse model code -- e.g.
"Bambu PLA Basic @BBL A1 Mini ..." is now
"Bambu PLA Basic @BBL A1M ...". User-authored profiles still use the
long form. Bambuddy's filters did a verbatim uppercase compare
("A1M" vs "A1 MINI"), so every renamed cloud profile silently
disappeared from the picker.
Centralize the alias check in slicerPrinterMatch.ts. New
PRINTER_MODEL_SUFFIX_ALIASES table maps "A1 Mini" <-> "A1M"
bidirectionally; exported matchesPrinterModelSuffix() does the
case-insensitive compare with the alias fallback. Two consumer
sites swap to the helper:
* ConfigureAmsSlotModal.tsx (Orca cloud and Bambu cloud filter
branches) -- the AMS slot picker, hit directly and reached from
SpoolBuddy's AMS page via mapModelCode(printer?.model)
* slicerPrinterMatch.ts:classifyByBambuName -- the SliceModal
Process / Filament compatibility check
Backend printer_models.py also gets a "Bambu Lab A1M" -> "A1 Mini"
entry so server-side 3MF model normalization stays consistent if a
3MF ever embeds the short form.
Kept the alias table narrow on purpose. Wide-net aliasing (e.g.
"X1" <-> "X1C") would silently collapse physically distinct
printers. When Bambu introduces the next rename, it is one new row
in the table -- /api/v1/cloud/settings is the place to grep, called
out in the source comment.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -15,6 +15,9 @@ PRINTER_MODEL_MAP = {
|
||||
"Bambu Lab A1": "A1",
|
||||
"Bambu Lab A1 Mini": "A1 Mini",
|
||||
"Bambu Lab A1 mini": "A1 Mini",
|
||||
# Bambu cloud rolled out a terse model-code rename mid-2026 (#1649);
|
||||
# 3MFs prepared with newer cloud presets may carry this short form.
|
||||
"Bambu Lab A1M": "A1 Mini",
|
||||
"Bambu Lab H2D": "H2D",
|
||||
"Bambu Lab H2D Pro": "H2D Pro",
|
||||
"Bambu Lab H2C": "H2C",
|
||||
|
||||
@@ -323,6 +323,37 @@ describe('ConfigureAmsSlotModal', () => {
|
||||
expect(screen.queryByText(/Bambu PLA Basic @BBL X1C/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('treats Bambu cloud rename @BBL A1M as a match for A1 Mini (#1649)', async () => {
|
||||
// Bambu cloud shifted A1 Mini filament profiles from
|
||||
// "Bambu PLA Basic @BBL A1 Mini ..." to the terse "@BBL A1M" mid-2026.
|
||||
// Without an alias-aware compare, the model filter strips every cloud
|
||||
// profile from the picker when the user selects an A1 Mini printer.
|
||||
(api.getCloudSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
filament: [
|
||||
{ setting_id: 'GFA00_A1M', name: 'Bambu PLA Basic @BBL A1M', filament_id: 'GFA00' },
|
||||
{ setting_id: 'GFA00_A1', name: 'Bambu PLA Basic @BBL A1', filament_id: 'GFA00' },
|
||||
],
|
||||
});
|
||||
render(<ConfigureAmsSlotModal {...defaultProps} printerModel="A1 Mini" />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Bambu PLA Basic @BBL A1M')).toBeInTheDocument();
|
||||
});
|
||||
// The A1 (non-mini) preset must still be filtered out — the alias
|
||||
// table must not collapse two physically distinct printers.
|
||||
expect(screen.queryByText('Bambu PLA Basic @BBL A1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('still filters cross-model cloud profiles when the printer is A1 Mini', async () => {
|
||||
// Sanity check that the alias addition didn't accidentally widen the
|
||||
// matcher: an X1C cloud preset stays hidden when the picker is for an
|
||||
// A1 Mini printer.
|
||||
render(<ConfigureAmsSlotModal {...defaultProps} printerModel="A1 Mini" />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Configure AMS/)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('Bambu PLA Basic @BBL X1C')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows current preset even when it does not match model filter', async () => {
|
||||
// Render with printerModel="H2D" but savedPresetId pointing to the X1C preset
|
||||
const slotInfo = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildCompatibilityIndex,
|
||||
matchesPrinterModelSuffix,
|
||||
presetCompatibility,
|
||||
EMPTY_COMPATIBILITY_INDEX,
|
||||
type CompatibilityBundle,
|
||||
@@ -425,3 +426,63 @@ describe('presetCompatibility — nozzle filtering on @BBL name fallback', () =>
|
||||
).toBe('mismatch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchesPrinterModelSuffix (#1649)', () => {
|
||||
it('matches the canonical short code against itself', () => {
|
||||
expect(matchesPrinterModelSuffix('X1C', 'X1C')).toBe(true);
|
||||
});
|
||||
|
||||
it('is case-insensitive on both sides', () => {
|
||||
expect(matchesPrinterModelSuffix('x1c', 'X1C')).toBe(true);
|
||||
expect(matchesPrinterModelSuffix('a1 mini', 'A1 Mini')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches Bambu cloud rename A1M against the long form A1 Mini', () => {
|
||||
expect(matchesPrinterModelSuffix('A1M', 'A1 Mini')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches the long form A1 Mini against the short Bambu cloud code A1M', () => {
|
||||
expect(matchesPrinterModelSuffix('A1 Mini', 'A1M')).toBe(true);
|
||||
});
|
||||
|
||||
it('does NOT match A1M against A1 (different printer, must not collapse)', () => {
|
||||
expect(matchesPrinterModelSuffix('A1M', 'A1')).toBe(false);
|
||||
});
|
||||
|
||||
it('does NOT match A1 against A1 Mini', () => {
|
||||
expect(matchesPrinterModelSuffix('A1', 'A1 Mini')).toBe(false);
|
||||
});
|
||||
|
||||
it('does NOT match unrelated models', () => {
|
||||
expect(matchesPrinterModelSuffix('X1C', 'P1S')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('presetCompatibility with Bambu cloud A1M rename (#1649)', () => {
|
||||
const A1_MINI = 'Bambu Lab A1 mini 0.4 nozzle';
|
||||
const A1 = 'Bambu Lab A1 0.4 nozzle';
|
||||
const idx = buildCompatibilityIndex([], PRINTER_MODELS);
|
||||
|
||||
it('matches a cloud preset using the new @BBL A1M suffix against an A1 Mini printer', () => {
|
||||
// The slicer-mirrored case: technopaw's report — A1 Mini cloud presets
|
||||
// newly ship as "Bambu PLA Basic @BBL A1M ..." and used to be filtered
|
||||
// out by the model check that compared "A1M" vs "A1 mini" verbatim.
|
||||
expect(
|
||||
presetCompatibility({ name: 'Bambu PLA Basic @BBL A1M' }, 'filament', A1_MINI, idx),
|
||||
).toBe('match');
|
||||
});
|
||||
|
||||
it('matches a 0.4-nozzle process with @BBL A1M against an A1 Mini printer', () => {
|
||||
expect(
|
||||
presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1_MINI, idx),
|
||||
).toBe('match');
|
||||
});
|
||||
|
||||
it('does NOT match @BBL A1M against an A1 (non-mini) printer', () => {
|
||||
// The alias must not collapse two physically different printers — A1
|
||||
// and A1 Mini ship distinct profile sets.
|
||||
expect(
|
||||
presetCompatibility({ name: '0.20mm Standard @BBL A1M' }, 'process', A1, idx),
|
||||
).toBe('mismatch');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { X, Loader2, Settings2, ChevronDown, CheckCircle2, RotateCcw } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { KProfile } from '../api/client';
|
||||
import { matchesPrinterModelSuffix } from '../utils/slicerPrinterMatch';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface SlotInfo {
|
||||
@@ -583,7 +584,7 @@ export function ConfigureAmsSlotModal({
|
||||
if (query && !op.name.toLowerCase().includes(query)) continue;
|
||||
if (printerModel) {
|
||||
const presetModel = extractPresetModel(op.name);
|
||||
if (presetModel && presetModel.toUpperCase() !== printerModel.toUpperCase()) continue;
|
||||
if (presetModel && !matchesPrinterModelSuffix(presetModel, printerModel)) continue;
|
||||
}
|
||||
// All Orca Cloud profiles are user-authored, so isUser is always true.
|
||||
items.push({ id: orcaId, name: op.name, source: 'orca_cloud', isUser: true });
|
||||
@@ -601,10 +602,12 @@ export function ConfigureAmsSlotModal({
|
||||
|| (trayIdx && (cp.setting_id === trayIdx || convertToTrayInfoIdx(cp.setting_id) === trayIdx));
|
||||
// Search filter applies to ALL presets (including saved) — no bypass
|
||||
if (query && !cp.name.toLowerCase().includes(query)) continue;
|
||||
// Filter by printer model if set (skip for current preset)
|
||||
// Filter by printer model if set (skip for current preset). Uses the
|
||||
// alias-aware match so Bambu's "A1 Mini" → "A1M" cloud rename (#1649)
|
||||
// doesn't hide A1 Mini cloud profiles.
|
||||
if (!isCurrentPreset && printerModel) {
|
||||
const presetModel = extractPresetModel(cp.name);
|
||||
if (presetModel && presetModel.toUpperCase() !== printerModel.toUpperCase()) continue;
|
||||
if (presetModel && !matchesPrinterModelSuffix(presetModel, printerModel)) continue;
|
||||
}
|
||||
items.push({ id: cp.setting_id, name: cp.name, source: 'cloud', isUser: isUserPreset(cp.setting_id) });
|
||||
}
|
||||
|
||||
@@ -62,6 +62,35 @@ function normalizePresetName(name: string): string {
|
||||
return name.replace(/^#\s*/, '').trim();
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -187,7 +216,14 @@ function classifyByBambuName(
|
||||
const inferredModel = bambuModelByShortCode[parsed.token] ?? parsed.token;
|
||||
const selectedParts = extractPrinterPresetModel(selectedPrinterName);
|
||||
if (!selectedParts) return 'unknown';
|
||||
if (normalizeModelFragment(selectedParts.model) !== normalizeModelFragment(inferredModel)) {
|
||||
// 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
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DMeff9V8.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BCHcur4g.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DgecYhis.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user