Explorar el Código

fix(slicer): filter process / filament presets by nozzle diameter too (#1325 follow-up #2)

  After the @BBL name fallback landed, IndividualGhost1905 reported that
  an X2D 0.4 selection still mixed 0.2 / 0.6 / 0.8 nozzle process variants
  into the main dropdown. The fallback's two extractors —
  extractPrinterPresetModel and extractBblToken — both ended their regex
  with `\s+[\d.]+\s*nozzle\s*$` and discarded the match, reducing
  "Bambu Lab X2D 0.4 nozzle" and "0.40mm Strength @BBL X2D 0.8 nozzle"
  to the same "X2D" string. Match was model-only; nozzle ignored.

  Bambu's naming convention: 0.4 is the default and DROPS the suffix; 0.2
  / 0.6 / 0.8 carry an explicit "<size> nozzle" segment. So a process
  preset with no suffix is implicitly 0.4 — not "any nozzle".

  Have both extractors return { model, nozzle }, parsing the suffix out
  instead of stripping. classifyByBambuName then requires both model AND
  nozzle to compare equal; a null process nozzle counts as "0.4" per the
  convention above. Differing nozzles fall into the existing "Other
  printers" group — no new group label.

  The bundle path was already nozzle-correct: a .bbscfg is scoped to one
  printer-preset-name including its nozzle, and the bundle-side exact
  match is therefore nozzle-aware. Only the @BBL name fallback needed
  fixing. The `compatible_printers` tier is also unaffected (Bambu's
  bundled `compatible_printers` lists include the full printer-preset
  name with nozzle, so disambiguation already works).

  If the selected printer preset name has no parseable nozzle (non-Bambu
  / hand-typed), the matcher degrades to model-only. Bambu printer
  presets always carry one in practice; this is defensive.

  9 new tests cover the matrix:
    - 0.4 printer ↔ no-suffix process: match
    - 0.4 printer ↔ 0.6 / 0.8 process: mismatch
    - 0.6 printer ↔ 0.6 process: match
    - 0.6 printer ↔ no-suffix process (=0.4): mismatch
    - same rule applied to filament presets
    - explicit "0.4 nozzle" suffix on process still matches 0.4 printer
    - wrong-model still mismatches even when nozzles agree
    - no-nozzle printer name degrades to model-only match

  One existing test ("handles a trailing nozzle-size suffix on the @BBL
  tag") had asserted that a 0.6-nozzle process matched a 0.4 printer —
  the exact reporter complaint. Reframed: matching 0.4-suffix still
  matches, 0.6-suffix now mismatches.
maziggy hace 4 días
padre
commit
6591fc011f

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
CHANGELOG.md


+ 105 - 1
frontend/src/__tests__/utils/slicerPrinterMatch.test.ts

@@ -246,6 +246,8 @@ describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
   });
   });
 
 
   it('handles a trailing nozzle-size suffix on the @BBL tag', () => {
   it('handles a trailing nozzle-size suffix on the @BBL tag', () => {
+    // An explicit "0.4 nozzle" suffix matches the 0.4 printer (Bambu's
+    // convention is to omit it for 0.4, but some cloud presets write it).
     expect(
     expect(
       presetCompatibility(
       presetCompatibility(
         { name: '0.20mm Standard @BBL X1C 0.4 nozzle' },
         { name: '0.20mm Standard @BBL X1C 0.4 nozzle' },
@@ -254,6 +256,11 @@ describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
         idx,
         idx,
       ),
       ),
     ).toBe('match');
     ).toBe('match');
+    // #1325 follow-up #2 (IndividualGhost1905, 2026-05-23): a different
+    // nozzle size IS a mismatch — a 0.6-nozzle process is unusable on a
+    // 0.4-nozzle printer. The dedicated "nozzle filtering" describe
+    // block below covers the full matrix; this case stays here as the
+    // counterpart to the matching-suffix case above.
     expect(
     expect(
       presetCompatibility(
       presetCompatibility(
         { name: '0.20mm Standard @BBL X1C 0.6 nozzle' },
         { name: '0.20mm Standard @BBL X1C 0.6 nozzle' },
@@ -261,7 +268,7 @@ describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
         X1C,
         X1C,
         idx,
         idx,
       ),
       ),
-    ).toBe('match');
+    ).toBe('mismatch');
   });
   });
 
 
   it('is unknown when the preset has no @BBL tag at all (custom name, no other signal)', () => {
   it('is unknown when the preset has no @BBL tag at all (custom name, no other signal)', () => {
@@ -321,3 +328,100 @@ describe('presetCompatibility — @BBL name fallback (no bundles)', () => {
     ).toBe('mismatch'); // X1C ≠ "X1 Carbon" without the registry
     ).toBe('mismatch'); // X1C ≠ "X1 Carbon" without the registry
   });
   });
 });
 });
+
+// #1325 follow-up #2 (IndividualGhost1905, 2026-05-23): the @BBL name
+// fallback must also filter by nozzle diameter. Bambu ships per-nozzle
+// variants of process / filament presets — 0.2 / 0.4 / 0.6 / 0.8 —
+// and a 0.6-nozzle process is unusable on a 0.4-nozzle printer. Bambu's
+// naming convention: 0.4 is the default and DROPS the suffix; 0.2 / 0.6
+// / 0.8 carry an explicit "<size> nozzle" segment. So an empty suffix
+// means 0.4, not "any nozzle".
+describe('presetCompatibility — nozzle filtering on @BBL name fallback', () => {
+  const X1C_04 = 'Bambu Lab X1 Carbon 0.4 nozzle';
+  const X1C_06 = 'Bambu Lab X1 Carbon 0.6 nozzle';
+  const X1C_08 = 'Bambu Lab X1 Carbon 0.8 nozzle';
+  // No bundles uploaded — exercise the @BBL fallback in isolation.
+  const index = buildCompatibilityIndex([], PRINTER_MODELS);
+
+  it('treats a no-suffix process as 0.4 (Bambu default) and matches a 0.4 printer', () => {
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C_04, index),
+    ).toBe('match');
+  });
+
+  it('flags a 0.6-nozzle process as mismatch against a 0.4 printer', () => {
+    expect(
+      presetCompatibility({ name: '0.30mm @BBL X1C 0.6 nozzle' }, 'process', X1C_04, index),
+    ).toBe('mismatch');
+  });
+
+  it('flags an 0.8-nozzle process as mismatch against a 0.4 printer', () => {
+    expect(
+      presetCompatibility({ name: '0.40mm Strength @BBL X1C 0.8 nozzle' }, 'process', X1C_04, index),
+    ).toBe('mismatch');
+  });
+
+  it('matches a 0.6-nozzle process against a 0.6 printer', () => {
+    expect(
+      presetCompatibility({ name: '0.30mm @BBL X1C 0.6 nozzle' }, 'process', X1C_06, index),
+    ).toBe('match');
+  });
+
+  it('flags a no-suffix process (=0.4) as mismatch against a 0.6 printer', () => {
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C_06, index),
+    ).toBe('mismatch');
+  });
+
+  it('applies the same rule to filament presets', () => {
+    // Bambu's bundled filament presets follow the same per-nozzle naming.
+    expect(
+      presetCompatibility(
+        { name: 'Bambu PLA Basic @BBL X1C 0.6 nozzle' },
+        'filament',
+        X1C_04,
+        index,
+      ),
+    ).toBe('mismatch');
+    expect(
+      presetCompatibility({ name: 'Bambu PLA Basic @BBL X1C' }, 'filament', X1C_04, index),
+    ).toBe('match');
+  });
+
+  it('keeps a 0.4 process matching a 0.4 printer when the preset DOES carry an explicit "0.4 nozzle" suffix', () => {
+    // Some cloud presets write the 0.4 suffix explicitly even though
+    // Bambu's bundled convention omits it. Both forms must compare equal.
+    expect(
+      presetCompatibility(
+        { name: '0.20mm Standard @BBL X1C 0.4 nozzle' },
+        'process',
+        X1C_04,
+        index,
+      ),
+    ).toBe('match');
+  });
+
+  it('still flags a wrong-MODEL process even when the nozzle matches', () => {
+    // The model filter must continue to dominate over the nozzle filter:
+    // a 0.4 A1 process isn't usable on an X1C 0.4 just because both are 0.4.
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL A1' }, 'process', X1C_04, index),
+    ).toBe('mismatch');
+  });
+
+  it('falls back to model-only when the selected printer name has no parseable nozzle', () => {
+    // Defensive degrade for non-Bambu / hand-typed names that happen to
+    // match the model. Real Bambu printer presets always carry a nozzle,
+    // so this path is rare; the assertion pins the intentional behaviour.
+    const noNozzle = 'Bambu Lab X1 Carbon'; // no "0.4 nozzle" suffix
+    expect(
+      presetCompatibility({ name: '0.30mm @BBL X1C 0.6 nozzle' }, 'process', noNozzle, index),
+    ).toBe('match');
+  });
+
+  it('flags 0.4 process on 0.8 printer (sanity check across the third common size)', () => {
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', X1C_08, index),
+    ).toBe('mismatch');
+  });
+});

+ 53 - 20
frontend/src/utils/slicerPrinterMatch.ts

@@ -121,38 +121,62 @@ function normalizeModelFragment(s: string): string {
   return s.replace(/\s+/g, '').toLowerCase();
   return s.replace(/\s+/g, '').toLowerCase();
 }
 }
 
 
-// Pull the model token out of a "@BBL <token> [0.4 nozzle]" suffix. The token
-// may contain a space (e.g. "A1 mini") so we strip a trailing nozzle-size
-// segment rather than splitting on the first whitespace.
-function extractBblToken(presetName: string): string | null {
+// 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 marker = '@BBL ';
   const idx = presetName.indexOf(marker);
   const idx = presetName.indexOf(marker);
   if (idx < 0) return null;
   if (idx < 0) return null;
   const rest = presetName.slice(idx + marker.length).trim();
   const rest = presetName.slice(idx + marker.length).trim();
-  const cleaned = rest.replace(/\s+[\d.]+\s*nozzle\s*$/i, '').trim();
-  return cleaned || null;
+  const { stripped, nozzle } = takeNozzleSuffix(rest);
+  return stripped ? { token: stripped, nozzle } : null;
 }
 }
 
 
-// Pull the model fragment out of a "Bambu Lab <model> [0.4 nozzle]" printer
-// preset name. Returns null for non-Bambu printer presets — there is no
-// reliable name-based match against those.
-function extractPrinterPresetModel(printerPresetName: string): string | null {
-  const m = printerPresetName.match(/^Bambu Lab\s+(.+?)(?:\s+[\d.]+\s*nozzle)?\s*$/i);
-  return m ? m[1].trim() : 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>`
  * Name-based fallback for presets BambuStudio ships with a `@BBL <model>`
  * tag (#1325 follow-up). Used only after `compatible_printers` and the
  * tag (#1325 follow-up). Used only after `compatible_printers` and the
  * uploaded-bundle index have already returned `'unknown'`.
  * uploaded-bundle index have already 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(
 function classifyByBambuName(
   presetName: string,
   presetName: string,
   selectedPrinterName: string,
   selectedPrinterName: string,
   bambuModelByShortCode: Record<string, string>,
   bambuModelByShortCode: Record<string, string>,
 ): PrinterCompatibility {
 ): PrinterCompatibility {
-  const token = extractBblToken(presetName);
-  if (!token) return 'unknown';
+  const parsed = extractBblToken(presetName);
+  if (!parsed) return 'unknown';
   // If the token isn't in the table (a brand-new Bambu model whose short
   // 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
   // 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
   // loaded yet), fall back to comparing the raw token. That keeps the
@@ -160,12 +184,21 @@ function classifyByBambuName(
   // identical — e.g. "Q1" preset against "Bambu Lab Q1 0.4 nozzle" —
   // 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
   // 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.
   // (X1C vs "X1 Carbon"), the registry is what makes the match work.
-  const inferredModel = bambuModelByShortCode[token] ?? token;
-  const selectedModel = extractPrinterPresetModel(selectedPrinterName);
-  if (!selectedModel) return 'unknown';
-  return normalizeModelFragment(selectedModel) === normalizeModelFragment(inferredModel)
-    ? 'match'
-    : 'mismatch';
+  const inferredModel = bambuModelByShortCode[parsed.token] ?? parsed.token;
+  const selectedParts = extractPrinterPresetModel(selectedPrinterName);
+  if (!selectedParts) return 'unknown';
+  if (normalizeModelFragment(selectedParts.model) !== normalizeModelFragment(inferredModel)) {
+    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';
 }
 }
 
 
 /**
 /**

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-8qalZ11b.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-IAFhhAbT.js"></script>
+    <script type="module" crossorigin src="/assets/index-8qalZ11b.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-HqxudbTf.css">
     <link rel="stylesheet" crossorigin href="/assets/index-HqxudbTf.css">
   </head>
   </head>
   <body>
   <body>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio