Просмотр исходного кода

fix(filament): a unique preset match no longer counts as a colour match (#2687)

The Filament Mapping panel reported "(Ready)" with a green tick for a slot
where the slice wanted dark red and the auto-matched tray held dark green.
Manually picking that same tray reported the mismatch correctly, which is
what made it obvious something was inconsistent.

Auto-match ranks candidates by tray_info_idx first, and a uniquely-matching
preset was accepted as definitive on the premise "same preset = same spool =
same colour". The preset names the variant, not the spool: GFA00 is PLA
Basic, GFA01 PLA Matte, GFA17 PLA Translucent, in every colour Bambu sells.
The reporter's own bundle has eight GFA00 trays in eight colours. With one
Matte spool loaded, every Matte requirement idx-matched it and the colour
comparison was never reached - which is why this surfaced on PLA Matte and
not on Basic, where several spools are usually loaded and the match falls
through to the branch that does compare colours.

The verdict now comes from the tray that was selected rather than from which
rule selected it, and both branches share one comparison so they cannot
drift apart again. Selection is unchanged - the right variant still wins per
mismatch and the slot stays selected.

A requirement with no colour at all is treated as satisfied rather than
mismatched; 3MFs that omit it parse to "" and there is nothing to disagree
with. That also affects the manual branch, which used to flag it.

No dispatch change: _get_missing_force_color_slots already required an exact
colour, so force colour match was gated correctly throughout.
maziggy 1 месяц назад
Родитель
Сommit
9ef06449ef

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 110 - 0
frontend/src/__tests__/hooks/useFilamentMapping.test.ts

@@ -1287,3 +1287,113 @@ describe('useFilamentMapping — no [-1] mapping during a status-load race (#258
     expect(result.current.hasTypeMismatch).toBe(true);
   });
 });
+
+describe('colour verdict is independent of how the tray was found (#2687)', () => {
+  // tray_info_idx names the filament *variant*, not an individual spool:
+  // GFA00 = PLA Basic, GFA01 = PLA Matte, GFA17 = PLA Translucent. A user with
+  // exactly one Matte spool loaded therefore idx-matches every Matte
+  // requirement no matter what colour it is.
+  const MATTE_DARK_GREEN = createPrinterStatus([
+    { id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_color: '004225', tray_info_idx: 'GFA01' }] },
+  ]);
+  const wantRedMatte = {
+    filaments: [
+      { slot_id: 1, type: 'PLA', color: '#9D432C', used_grams: 31, tray_info_idx: 'GFA01' },
+    ],
+  };
+
+  it('reports a unique-idx tray of the wrong colour as type_only, not match', () => {
+    const [item] = buildFilamentComparison(
+      wantRedMatte,
+      buildLoadedFilaments(MATTE_DARK_GREEN),
+      {},
+    );
+
+    // The tray is still selected — it is the right variant (#2650) ...
+    expect(item.loaded?.globalTrayId).toBe(0);
+    // ... but red-on-dark-green is not a colour match.
+    expect(item.colorMatch).toBe(false);
+    expect(item.status).toBe('type_only');
+  });
+
+  it('auto and manual agree on the same tray', () => {
+    const loaded = buildLoadedFilaments(MATTE_DARK_GREEN);
+    const auto = buildFilamentComparison(wantRedMatte, loaded, {})[0];
+    const manual = buildFilamentComparison(wantRedMatte, loaded, { 1: 0 })[0];
+
+    // The original report: auto said "match", manually picking that very tray
+    // said "mismatch". Both paths must now reach the same verdict.
+    expect(manual.isManual).toBe(true);
+    expect(auto.status).toBe(manual.status);
+    expect(auto.colorMatch).toBe(manual.colorMatch);
+  });
+
+  it('surfaces the mismatch through the hook so the panel stops saying Ready', () => {
+    const { result } = renderHook(() => useFilamentMapping(wantRedMatte, MATTE_DARK_GREEN, {}));
+    // hasColorMismatch drives the yellow "(Color mismatch)" header; the tray is
+    // still mapped, so this is not a type mismatch.
+    expect(result.current.hasColorMismatch).toBe(true);
+    expect(result.current.hasTypeMismatch).toBe(false);
+    expect(result.current.amsMapping).toEqual([0]);
+  });
+
+  it('still reports a match when the unique-idx tray does carry the right colour', () => {
+    const [item] = buildFilamentComparison(
+      { filaments: [{ slot_id: 1, type: 'PLA', color: '#004225', used_grams: 31, tray_info_idx: 'GFA01' }] },
+      buildLoadedFilaments(MATTE_DARK_GREEN),
+      {},
+    );
+    expect(item.status).toBe('match');
+    expect(item.colorMatch).toBe(true);
+  });
+
+  it('accepts a near-enough shade on the idx path', () => {
+    // Within colorsAreSimilar's per-channel tolerance — the printer reporting a
+    // spool a shade off must not become a mismatch.
+    const [item] = buildFilamentComparison(
+      { filaments: [{ slot_id: 1, type: 'PLA', color: '#0A4A2A', used_grams: 31, tray_info_idx: 'GFA01' }] },
+      buildLoadedFilaments(MATTE_DARK_GREEN),
+      {},
+    );
+    expect(item.status).toBe('match');
+  });
+
+  it('treats a colourless requirement as satisfied by any colour', () => {
+    // 3MFs that omit the colour parse to "" (filament_requirements.py); there is
+    // nothing to disagree with, so this must not read as a colour mismatch.
+    const [item] = buildFilamentComparison(
+      { filaments: [{ slot_id: 1, type: 'PLA', color: '', used_grams: 31, tray_info_idx: 'GFA01' }] },
+      buildLoadedFilaments(MATTE_DARK_GREEN),
+      {},
+    );
+    expect(item.status).toBe('match');
+    expect(item.colorMatch).toBe(true);
+  });
+
+  it('keeps the multi-idx path intact — same idx, several colours picks the right one', () => {
+    // Two Matte spools: the branch that already compared colours must be
+    // unaffected, and the exact-colour tray still wins.
+    const twoMatte = createPrinterStatus([
+      {
+        id: 0,
+        tray: [
+          { id: 0, tray_type: 'PLA', tray_color: '004225', tray_info_idx: 'GFA01' },
+          { id: 1, tray_type: 'PLA', tray_color: '9D432C', tray_info_idx: 'GFA01' },
+        ],
+      },
+    ]);
+    const [item] = buildFilamentComparison(wantRedMatte, buildLoadedFilaments(twoMatte), {});
+    expect(item.loaded?.globalTrayId).toBe(1);
+    expect(item.status).toBe('match');
+  });
+
+  it('a type-only fallback with no idx candidate is still type_only', () => {
+    // Regression guard: the pre-existing "type matches, colour does not" path.
+    const basicOnly = createPrinterStatus([
+      { id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_color: '004225', tray_info_idx: 'GFA00' }] },
+    ]);
+    const [item] = buildFilamentComparison(wantRedMatte, buildLoadedFilaments(basicOnly), {});
+    expect(item.status).toBe('type_only');
+    expect(item.colorMatch).toBe(false);
+  });
+});

+ 38 - 11
frontend/src/hooks/useFilamentMapping.ts

@@ -203,6 +203,27 @@ export function useLoadedFilaments(
   }, [printerStatus]);
 }
 
+/**
+ * Does the tray we picked actually carry the colour the slice asked for?
+ *
+ * Shared by the manual and auto branches below so the two can never disagree
+ * about the same tray again (#2687). Exact hex first, then the perceptual
+ * tolerance, so a spool the printer reports one shade off still reads as a
+ * match.
+ *
+ * A requirement with no colour at all is not a mismatch — the 3MF simply
+ * didn't ask for one (`filament_requirements.py` defaults it to `""`), so any
+ * loaded colour satisfies it. Loaded trays always have a colour: buildLoaded-
+ * Filaments falls back to grey when MQTT reports none.
+ */
+function coloursMatch(loadedColor: string | undefined, requiredColor: string | undefined): boolean {
+  const required = normalizeColorForCompare(requiredColor);
+  if (!required) return true;
+  return (
+    normalizeColorForCompare(loadedColor) === required || colorsAreSimilar(loadedColor, requiredColor)
+  );
+}
+
 /**
  * Compare required filaments with loaded filaments (non-hook version).
  *
@@ -236,9 +257,7 @@ export function buildFilamentComparison(
 
       if (manualLoaded) {
         const typeMatch = manualLoaded.type?.toUpperCase() === req.type?.toUpperCase();
-        const colorMatch =
-          normalizeColorForCompare(manualLoaded.color) === normalizeColorForCompare(req.color) ||
-          colorsAreSimilar(manualLoaded.color, req.color);
+        const colorMatch = coloursMatch(manualLoaded.color, req.color);
 
         let status: FilamentStatus;
         if (typeMatch && colorMatch) {
@@ -358,17 +377,25 @@ export function buildFilamentComparison(
 
     const hasFilament = !!loaded;
     const typeMatch = hasFilament;
-    // idxMatch is always considered a color match (same spool = same color)
-    const colorMatch = !!idxMatch || !!exactMatch || !!similarMatch;
-
-    // Status: match (tray_info_idx, type+color, or similar color), type_only (type ok, color very different), mismatch (type not found)
+    // #2687: judge the colour on the tray we actually picked, never on which
+    // branch found it. tray_info_idx identifies the filament *variant* — GFA00
+    // is PLA Basic, GFA01 PLA Matte, GFA17 PLA Translucent — not an individual
+    // spool, so one Matte spool idx-matches every Matte requirement whatever
+    // colour it is. The old rule ("same spool = same color") therefore reported
+    // red-required-on-green-loaded as a match, while manually picking that same
+    // tray reported the mismatch honestly. Variant still decides *selection*
+    // (#2650: Basic is not Matte) — it just no longer decides the verdict.
+    const colorMatch = hasFilament && coloursMatch(loaded.color, req.color);
+
+    // No tray of the required type at all is a type mismatch; otherwise the
+    // colour decides between a full match and type-only.
     let status: FilamentStatus;
-    if (idxMatch || exactMatch || similarMatch) {
+    if (!hasFilament) {
+      status = 'mismatch';
+    } else if (colorMatch) {
       status = 'match';
-    } else if (typeOnlyMatch) {
-      status = 'type_only';
     } else {
-      status = 'mismatch';
+      status = 'type_only';
     }
 
     return {

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-B-Fe9tPc.js


+ 1 - 1
static/index.html

@@ -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-DAvf4vFq.js"></script>
+    <script type="module" crossorigin src="/assets/index-B-Fe9tPc.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-D4bpNaiw.css">
   </head>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов