Browse Source

Queue: force colour on a printer job, two edge cases

- A tick left in the dialog after a second printer was added (its box
  is only shown for one printer) was saved for every printer and held
  their jobs against the trays picked for them. Saved only while one
  printer is selected.
- A ticked slot whose chosen tray can't be read at save time (printer
  off, tray emptied) asked for the sliced filament and held the job
  against the user's pick. Saved unforced, as before.
- CHANGELOG: a tick on a tray the dialog matched itself waits for the
  file's colour.
maziggy 3 days ago
parent
commit
621e104bf2

+ 1 - 1
CHANGELOG.md

@@ -219,7 +219,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 
 ### Fixed
 ### Fixed
-- **"Force color match" is kept on a job for a specific printer (#3445, reported by @frnzzle)** — With a specific printer picked, the print dialog showed the "Force color match" checkbox under each slot but never saved it: the job was added or saved with its trays only, and the box came back unticked. Once the tray picked for a slot no longer fit when the job came up, the scheduler matched the trays again against the colour the file was sliced with and, without that colour loaded, took any spool of the type, with "prefer lowest" the emptiest. A part sliced in white and set to black printed in green. A ticked slot is now saved with the filament of the tray picked for it, so the box stays ticked, and matching again takes only that filament or holds the job. Jobs for "Any" model were not affected.
+- **"Force color match" is kept on a job for a specific printer (#3445, reported by @frnzzle)** — With a specific printer picked, the print dialog showed the "Force color match" checkbox under each slot but never saved it: the job was added or saved with its trays only, and the box came back unticked. Once the tray picked for a slot no longer fit when the job came up, the scheduler matched the trays again against the colour the file was sliced with and, without that colour loaded, took any spool of the type, with "prefer lowest" the emptiest. A part sliced in white and set to black printed in green. A ticked slot is now saved, so the box stays ticked: set to a tray you picked, it asks for that tray's filament; on a tray the dialog matched by itself, for the file's own, so the job waits for it. Matching again takes only that filament or holds the job. Jobs for "Any" model were not affected.
 - **Configure accepts a setting id as the filament (#3444, reported by @Novastorm4332)** — A client of the REST API that configured a slot with a spool's slicer filament as `tray_info_idx` sent a Bambu setting id such as `GFSL99` to the printer, which knows only filament ids (`GFL99`): the slot came back without a colour or type and auto-unlink dropped the spool. Assigning the spool already converted it. Configure now converts it the same way, the setting id staying as the slot's setting id, and does the same for a K profile's filament id. Support and SUNLU ids, which also begin with GFS, are filament ids and stay as they are. Bambuddy's own Configure dialog was not affected.
 - **Configure accepts a setting id as the filament (#3444, reported by @Novastorm4332)** — A client of the REST API that configured a slot with a spool's slicer filament as `tray_info_idx` sent a Bambu setting id such as `GFSL99` to the printer, which knows only filament ids (`GFL99`): the slot came back without a colour or type and auto-unlink dropped the spool. Assigning the spool already converted it. Configure now converts it the same way, the setting id staying as the slot's setting id, and does the same for a K profile's filament id. Support and SUNLU ids, which also begin with GFS, are filament ids and stay as they are. Bambuddy's own Configure dialog was not affected.
 - **A project saved as .gcode.3mf is treated as a project (#3435, reported by @Mounting9612)** — Bambu Studio saves an unsliced project under whatever name it is given, and one called `Foo.gcode.3mf` went into the File Manager as a printable file. Printing it to a cost centre was refused with "Estimated cost is required for cost center prints", which says nothing about the cause; without billing it would have gone to the printer. A `.gcode.3mf` that holds no G-code is now stored as a project and offered for slicing, files already in the library are re-checked once on the update, and the queue refuses such a file with "… isn't sliced yet. Slice it first, then print the sliced file." The File Manager's Add to Queue judges a file by what it holds too, so a sliced plate exported as a plain `.3mf` can be queued from there.
 - **A project saved as .gcode.3mf is treated as a project (#3435, reported by @Mounting9612)** — Bambu Studio saves an unsliced project under whatever name it is given, and one called `Foo.gcode.3mf` went into the File Manager as a printable file. Printing it to a cost centre was refused with "Estimated cost is required for cost center prints", which says nothing about the cause; without billing it would have gone to the printer. A `.gcode.3mf` that holds no G-code is now stored as a project and offered for slicing, files already in the library are re-checked once on the update, and the queue refuses such a file with "… isn't sliced yet. Slice it first, then print the sliced file." The File Manager's Add to Queue judges a file by what it holds too, so a sliced plate exported as a plain `.3mf` can be queued from there.
 - **A spool swapped in mid-print shows on the printer card (#3434, reported by @0xCyberFortress)** — After a runout, a new spool loaded into the same slot and assigned in Bambuddy stayed invisible until the print ended: the printer reports the slot loaded but blank and refuses a filament setting for the slot it is printing from, so the card showed "?" and "No filament assigned". The card now shows the assigned spool on such a slot in its colour, with an amber marker, names it and offers Unassign, saying that the printer has not taken it yet. Bambuddy also stops sending that setting on every AMS change while the slot feeds the print, which the printer refused each time, and sends it once the print is over.
 - **A spool swapped in mid-print shows on the printer card (#3434, reported by @0xCyberFortress)** — After a runout, a new spool loaded into the same slot and assigned in Bambuddy stayed invisible until the print ended: the printer reports the slot loaded but blank and refuses a filament setting for the slot it is printing from, so the card showed "?" and "No filament assigned". The card now shows the assigned spool on such a slot in its colour, with an amber marker, names it and offers Unassign, saying that the printer has not taken it yet. Bambuddy also stops sending that setting on every AMS change while the slot feeds the print, which the printer refused each time, and sends it once the print is over.

+ 36 - 1
frontend/src/__tests__/components/PrintModal.test.tsx

@@ -3226,7 +3226,10 @@ describe('PrintModal default printer', () => {
 describe('PrintModal — "Force color match" on a specific printer is saved (#3445)', () => {
 describe('PrintModal — "Force color match" on a specific printer is saved (#3445)', () => {
   const mockOnClose = vi.fn();
   const mockOnClose = vi.fn();
 
 
-  const PRINTERS = [{ id: 1, name: 'Printer 01', model: 'P1S', ip_address: '192.168.1.101', enabled: true, is_active: true }];
+  const PRINTERS = [
+    { id: 1, name: 'Printer 01', model: 'P1S', ip_address: '192.168.1.101', enabled: true, is_active: true },
+    { id: 2, name: 'Printer 02', model: 'P1S', ip_address: '192.168.1.102', enabled: true, is_active: true },
+  ];
   // Sliced in white; the printer holds green (tray 0) and black (tray 1), no white.
   // Sliced in white; the printer holds green (tray 0) and black (tray 1), no white.
   const SLOT_1_WHITE = { slot_id: 1, type: 'PLA', color: '#CBC6B8', tray_info_idx: 'GFA01', used_grams: 50 };
   const SLOT_1_WHITE = { slot_id: 1, type: 'PLA', color: '#CBC6B8', tray_info_idx: 'GFA01', used_grams: 50 };
 
 
@@ -3394,6 +3397,38 @@ describe('PrintModal — "Force color match" on a specific printer is saved (#34
     ]);
     ]);
   });
   });
 
 
+  it("does not force a slot whose chosen tray can't be read", async () => {
+    // The printer is off: no trays to say what the slot is set to.
+    server.use(
+      http.get('/api/v1/printers/:id/status', () =>
+        HttpResponse.json({ connected: false, state: 'OFFLINE', ams: [], vt_tray: [] }),
+      ),
+    );
+    const user = userEvent.setup();
+    render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={printer01Item()} onClose={mockOnClose} />);
+
+    await user.click(await openMapping(user));
+    await submit(user);
+
+    await waitFor(() => expect(patched).toHaveLength(1));
+    // Not "wait for white": that would hold the job off the black tray picked for it.
+    expect(patched[0].filament_overrides ?? null).toBeNull();
+  });
+
+  it('does not save a tick whose box went away when a second printer was added', async () => {
+    const user = userEvent.setup();
+    render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={printer01Item()} onClose={mockOnClose} />);
+
+    await user.click(await openMapping(user));
+    await user.click(screen.getByText('Printer 02'));
+    await waitFor(() => expect(screen.queryByRole('checkbox', { name: /force color match/i })).not.toBeInTheDocument());
+    await submit(user);
+
+    // The edited job stays on printer 01, a copy goes to printer 02.
+    await waitFor(() => expect(patched.length + posted.length).toBe(2));
+    for (const job of [...patched, ...posted]) expect(job.filament_overrides ?? null).toBeNull();
+  });
+
   it('waits for the sliced colour when the box is ticked on a tray the dialog matched itself', async () => {
   it('waits for the sliced colour when the box is ticked on a tray the dialog matched itself', async () => {
     const user = userEvent.setup();
     const user = userEvent.setup();
     render(
     render(

+ 15 - 6
frontend/src/components/PrintModal/index.tsx

@@ -1308,20 +1308,29 @@ export function PrintModal({
     };
     };
     const printerOverridesForPlate = (printerId: number | null, plateId: number | null) => {
     const printerOverridesForPlate = (printerId: number | null, plateId: number | null) => {
       const loaded = printerId != null ? buildLoadedFilaments(statusForPrinter(printerId)) : [];
       const loaded = printerId != null ? buildLoadedFilaments(statusForPrinter(printerId)) : [];
+      // The box is only on screen with one printer selected; a tick left over
+      // from before more were added is not saved.
+      const ticksShown = selectedPrinters.length === 1;
       const entries = overridesForPlate(plateId)
       const entries = overridesForPlate(plateId)
-        ?.filter((o) => filamentOverrides[o.slot_id] || o.force_color_match)
-        .map((o) => {
+        ?.flatMap((o) => {
+          const changed = !!filamentOverrides[o.slot_id];
+          if (!changed && !(o.force_color_match && ticksShown)) return [];
           const tray = o.force_color_match && printerId != null ? chosenTray(printerId, plateId, o.slot_id) : undefined;
           const tray = o.force_color_match && printerId != null ? chosenTray(printerId, plateId, o.slot_id) : undefined;
-          const filament = tray != null ? loaded.find((f) => f.globalTrayId === tray) : undefined;
-          if (!filament) return o;
-          return {
+          if (tray == null) return [o];
+          const filament = loaded.find((f) => f.globalTrayId === tray);
+          // A chosen tray that can't be read now (printer off, tray emptied)
+          // says nothing about the filament wanted: asking for the sliced one
+          // would hold the job against the user's own pick. Saved unforced,
+          // as before #3445.
+          if (!filament) return changed ? [{ ...o, force_color_match: false }] : [];
+          return [{
             slot_id: o.slot_id,
             slot_id: o.slot_id,
             type: filament.type,
             type: filament.type,
             color: filament.color,
             color: filament.color,
             color_name: filament.colorName,
             color_name: filament.colorName,
             ...(filament.trayInfoIdx ? { tray_info_idx: filament.trayInfoIdx } : {}),
             ...(filament.trayInfoIdx ? { tray_info_idx: filament.trayInfoIdx } : {}),
             force_color_match: true,
             force_color_match: true,
-          };
+          }];
         });
         });
       return entries && entries.length > 0 ? entries : undefined;
       return entries && entries.length > 0 ? entries : undefined;
     };
     };