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

fix(colors): a colour set on the printer's screen is named after its Bambu colour (#3279)

- getColorName falls back to the nearest Bambu Lab catalogue colour
  (CIEDE2000 <= 3) before the hue bucket: 5A657B reads Blue Grey, not
  Blue.
- The search stays within the slot's product line when Bambu makes it;
  other lines are searched only for filament outside Bambu's ranges.
- The product line comes from tray_sub_brands or, without RFID, from
  the slot's preset id (tray_info_idx); printer cards, the slot
  mapping and the filament override list pass it.
- /inventory/colors/map also returns the Bambu rows (bambu) and the
  preset id -> product line table (preset_materials).
- Inventory spool names (resolveSpoolColorName) are unchanged.
maziggy 15 часов назад
Родитель
Сommit
eca23be325
55 измененных файлов с 507 добавлено и 28 удалено
  1. 4 0
      CHANGELOG.md
  2. 28 1
      backend/app/api/routes/inventory.py
  3. 65 1
      backend/tests/integration/test_color_map_api.py
  4. 29 0
      frontend/src/__tests__/contexts/ColorCatalogContext.test.tsx
  5. 29 2
      frontend/src/__tests__/hooks/useFilamentMapping.test.ts
  6. 122 0
      frontend/src/__tests__/pages/PrintersPageColorName3279.test.tsx
  7. 113 0
      frontend/src/__tests__/utils/colors.test.ts
  8. 7 1
      frontend/src/api/client.ts
  9. 1 1
      frontend/src/components/PrintModal/FilamentOverride.tsx
  10. 7 2
      frontend/src/contexts/ColorCatalogContext.tsx
  11. 2 2
      frontend/src/hooks/useFilamentMapping.ts
  12. 3 3
      frontend/src/pages/PrintersPage.tsx
  13. 86 4
      frontend/src/utils/colors.ts
  14. 0 0
      static/assets/AdditionalSection-B7m268Mn.js
  15. 0 0
      static/assets/AmsUnitCard-2znOVbbm.js
  16. 0 0
      static/assets/ArchivesPage-BDZRcdue.js
  17. 0 0
      static/assets/ArchivesPage-Cijsih6K.js
  18. 0 0
      static/assets/AssignToAmsModal-CPJpsziR.js
  19. 0 0
      static/assets/CameraPage-BbZPbJZX.js
  20. 0 0
      static/assets/ConfigureAmsSlotModal-BbvuPAz9.js
  21. 0 0
      static/assets/ConfirmOutcomeDialog-ct-PiJ6A.js
  22. 0 0
      static/assets/FileManagerPage-DRng2Pc4.js
  23. 0 0
      static/assets/FinancePage-DNe-MhiO.js
  24. 0 0
      static/assets/InventoryPage-BasVhHe0.js
  25. 0 0
      static/assets/InventoryPage-C2RpaALI.js
  26. 0 0
      static/assets/Layout-GVz6JdbD.js
  27. 0 0
      static/assets/ModelSourcesPage-CcJV2sxE.js
  28. 0 0
      static/assets/PhotoGalleryModal-BFs0lsfy.js
  29. 0 0
      static/assets/PrintModal-BtM1dElr.js
  30. 0 0
      static/assets/PrintModal-DJVIzPGw.js
  31. 0 0
      static/assets/PrintersPage-AhkqA1so.js
  32. 0 0
      static/assets/ProfilesPage-DqZcrLlV.js
  33. 0 0
      static/assets/ProjectDetailPage-cv8CSinR.js
  34. 0 0
      static/assets/QueuePage-CKosToJ2.js
  35. 0 0
      static/assets/QueuePage-D_6Y-y7F.js
  36. 0 0
      static/assets/RichTextEditor-CmV3FjN0.js
  37. 0 0
      static/assets/SettingsPage-Dg6DSQKt.js
  38. 0 0
      static/assets/SliceModal-Bkt50Poh.js
  39. 0 0
      static/assets/SpoolBuddyAmsPage-C3v9i05Y.js
  40. 0 0
      static/assets/SpoolBuddyAmsPage-C3y_u4o_.js
  41. 0 0
      static/assets/SpoolBuddyDashboard-XR2dlZLf.js
  42. 0 0
      static/assets/SpoolBuddyInventoryPage-Cu4JsLyK.js
  43. 1 1
      static/assets/SpoolBuddySettingsPage-LTDSPP37.js
  44. 0 0
      static/assets/SpoolBuddyWriteTagPage-CoKYc6Lr.js
  45. 0 0
      static/assets/StatsPage-C4rWdML5.js
  46. 0 0
      static/assets/amsHelpers-Dlwij40i.js
  47. 0 0
      static/assets/colors-BF4_h-XA.js
  48. 0 0
      static/assets/colors-je4aRt_-.js
  49. 0 0
      static/assets/index-WCWK8Sj1.js
  50. 1 1
      static/assets/inventorySearch-pLpY8YP6.js
  51. 0 0
      static/assets/locationSensorDefaults-B2J6PFof.js
  52. 1 0
      static/assets/useColorCatalogVersion-7yzLvEqw.js
  53. 0 1
      static/assets/useColorCatalogVersion-uixz-m0E.js
  54. 0 0
      static/assets/utils-B8CUydnj.js
  55. 8 8
      static/index.html

+ 4 - 0
CHANGELOG.md

@@ -169,6 +169,10 @@ 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
+- **A color set on the printer's touchscreen is named after the Bambu color it is (#3279, reported by @Lioncat55)** — A slot without an RFID tag reports a hex a step or two off Bambu Studio's, for example `#5A657B` for **Blue Grey** (`#5B6579`). No catalog entry matched it exactly, so the slot was called by a general name such as "Blue", while the printer's screen showed a gray.
+  - **Now:** when no catalog entry has exactly that hex, the color is named after the closest **Bambu Lab** color in the Color Catalog, as long as it is close enough to be the same color.
+  - **Within the slot's own range:** the range comes from the slot's material or, without RFID, from the filament preset picked on the printer's screen. A PETG Translucent slot is named from PETG Translucent colors, and a range Bambu makes never borrows a nearby color's name from another range. Only a filament outside Bambu's ranges is compared with all of them.
+  - **Where:** the AMS slot details on the printer cards, the print dialog and the queue. A hex with no Bambu color that close keeps its general name. Spool names in Inventory are unchanged.
 - **A job queued for "Any" model no longer keeps returning to a printer that can't map its filament** — When choosing a printer, the queue only checked that each filament type, and each forced colour, was loaded somewhere on it. On a dual-nozzle printer (H2D, H2C, X2D) without a filament track switch, a spool only feeds the nozzle it belongs to. A printer with the filament on the other nozzle was still chosen, and the job was held there and handed back. **Start** then let it go without offering **Print Anyway**, and the next pass chose the same printer again.
 - **A job queued for "Any" model no longer keeps returning to a printer that can't map its filament** — When choosing a printer, the queue only checked that each filament type, and each forced colour, was loaded somewhere on it. On a dual-nozzle printer (H2D, H2C, X2D) without a filament track switch, a spool only feeds the nozzle it belongs to. A printer with the filament on the other nozzle was still chosen, and the job was held there and handed back. **Start** then let it go without offering **Print Anyway**, and the next pass chose the same printer again.
   - **Now passed over:** a printer is only chosen when the job's filaments can be matched to its spools the way they are when the job is sent. Another printer of the model that has them gets the job.
   - **Now passed over:** a printer is only chosen when the job's filaments can be matched to its spools the way they are when the job is sent. Another printer of the model that has them gets the job.
   - **Also on single-nozzle printers:** the queue gives each slot its own spool, so two slots of the same material need two spools of it. A printer with only one spool of it was chosen and looped the same way.
   - **Also on single-nozzle printers:** the queue gives each slot its own spool, so two slots of the same material need two spools of it. A printer with only one spool of it was chosen and looped the same way.

+ 28 - 1
backend/app/api/routes/inventory.py

@@ -1032,7 +1032,21 @@ async def get_color_name_map(
     generic white PLA slot in place of Bambu's "Jade White", trading one
     generic white PLA slot in place of Bambu's "Jade White", trading one
     arbitrary answer for another. What survives is the handful of cases this
     arbitrary answer for another. What survives is the handful of cases this
     exists for.
     exists for.
+
+    ``bambu`` lists every Bambu Lab row as ``{hex, name, material}`` for naming
+    a colour by its nearest Bambu entry (#3279). A colour picked on the
+    printer's touchscreen is reported a step or two off the catalogue's hex
+    (5A657B for Blue Grey 5B6579), which no exact lookup finds. Only Bambu's
+    rows: that is the printer's palette, and another brand's name for a
+    neighbouring hex would be a guess.
+
+    ``preset_materials`` maps each Bambu preset id to its product line
+    (``GFA00`` -> ``PLA Basic``). A slot without RFID reports the preset picked
+    on the printer's screen but an empty ``tray_sub_brands``, and the product
+    line is what the nearest search looks in first.
     """
     """
+    from backend.app.api.routes.cloud import _BUILTIN_FILAMENT_NAMES
+
     result = await db.execute(
     result = await db.execute(
         select(
         select(
             ColorCatalogEntry.hex_color,
             ColorCatalogEntry.hex_color,
@@ -1045,6 +1059,7 @@ async def get_color_name_map(
     # hex → (name, priority, manufacturer); higher priority wins, first on a tie
     # hex → (name, priority, manufacturer); higher priority wins, first on a tie
     mapping: dict[str, tuple[str, int, str]] = {}
     mapping: dict[str, tuple[str, int, str]] = {}
     by_material: dict[str, tuple[str, int, str]] = {}  # "material|hex" → same
     by_material: dict[str, tuple[str, int, str]] = {}  # "material|hex" → same
+    bambu: list[dict[str, str]] = []
     for hex_color, color_name, manufacturer, is_default, material in result.all():
     for hex_color, color_name, manufacturer, is_default, material in result.all():
         if not hex_color or not color_name:
         if not hex_color or not color_name:
             continue
             continue
@@ -1052,6 +1067,8 @@ async def get_color_name_map(
         if len(key) != 6:
         if len(key) != 6:
             continue
             continue
         brand = (manufacturer or "").strip().lower()
         brand = (manufacturer or "").strip().lower()
+        if brand == "bambu lab":
+            bambu.append({"hex": key, "name": color_name, "material": (material or "").strip()})
         priority = 0
         priority = 0
         if brand == "bambu lab":
         if brand == "bambu lab":
             priority += 2
             priority += 2
@@ -1075,7 +1092,17 @@ async def get_color_name_map(
         flat = mapping.get(qualified.rsplit("|", 1)[1])
         flat = mapping.get(qualified.rsplit("|", 1)[1])
         if flat and flat[0] != name and flat[2] == brand:
         if flat and flat[0] != name and flat[2] == brand:
             qualified_colors[qualified] = name
             qualified_colors[qualified] = name
-    return {"colors": colors, "by_material": qualified_colors}
+    preset_materials = {
+        preset: name.removeprefix("Bambu ").strip()
+        for preset, name in _BUILTIN_FILAMENT_NAMES.items()
+        if name.startswith("Bambu ")
+    }
+    return {
+        "colors": colors,
+        "by_material": qualified_colors,
+        "bambu": bambu,
+        "preset_materials": preset_materials,
+    }
 
 
 
 
 @router.post("/colors", response_model=ColorEntryResponse)
 @router.post("/colors", response_model=ColorEntryResponse)

+ 65 - 1
backend/tests/integration/test_color_map_api.py

@@ -31,7 +31,7 @@ async def test_color_map_empty_catalog(async_client: AsyncClient):
     response = await async_client.get("/api/v1/inventory/colors/map")
     response = await async_client.get("/api/v1/inventory/colors/map")
     assert response.status_code == 200
     assert response.status_code == 200
     body = response.json()
     body = response.json()
-    assert body == {"colors": {}, "by_material": {}}
+    assert body["colors"] == {} and body["by_material"] == {} and body["bambu"] == []
 
 
 
 
 @pytest.mark.asyncio
 @pytest.mark.asyncio
@@ -411,3 +411,67 @@ async def test_a_material_containing_the_separator_is_still_read_correctly(async
     body = (await async_client.get("/api/v1/inventory/colors/map")).json()
     body = (await async_client.get("/api/v1/inventory/colors/map")).json()
 
 
     assert body["by_material"] == {"pla|matte|ffffff": "Ivory White"}
     assert body["by_material"] == {"pla|matte|ffffff": "Ivory White"}
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_color_map_lists_bambu_colours_for_nearest_naming(async_client: AsyncClient, db_session):
+    """#3279: a colour set on the printer's screen is reported as a hex a step or
+    two off the catalogue's (5A657B for Blue Grey 5B6579), so the frontend names
+    it by the nearest Bambu colour. It gets every Bambu row with its material,
+    and only Bambu's: other brands' names are not the printer's palette."""
+    await _seed(
+        db_session,
+        [
+            {
+                "manufacturer": "Bambu Lab",
+                "color_name": "Blue Grey",
+                "hex_color": "#5B6579",
+                "material": "PLA Basic",
+                "is_default": True,
+            },
+            {
+                "manufacturer": "bambu lab",
+                "color_name": "Translucent Orange",
+                "hex_color": "FF911AFF",
+                "material": "PETG Translucent",
+                "is_default": False,
+            },
+            {
+                "manufacturer": "eSUN",
+                "color_name": "Translucent Orange",
+                "hex_color": "#DD7135",
+                "material": "PETG",
+                "is_default": True,
+            },
+            {
+                "manufacturer": "Bambu Lab",
+                "color_name": "Broken",
+                "hex_color": "#12",
+                "material": "PLA Basic",
+                "is_default": True,
+            },
+        ],
+    )
+
+    response = await async_client.get("/api/v1/inventory/colors/map")
+
+    assert response.status_code == 200
+    assert sorted(response.json()["bambu"], key=lambda e: e["hex"]) == [
+        {"hex": "5b6579", "name": "Blue Grey", "material": "PLA Basic"},
+        {"hex": "ff911a", "name": "Translucent Orange", "material": "PETG Translucent"},
+    ]
+
+
+@pytest.mark.asyncio
+@pytest.mark.integration
+async def test_color_map_names_the_product_line_of_each_bambu_preset(async_client: AsyncClient):
+    """#3279: a slot without RFID reports its preset (GFA00) but no
+    tray_sub_brands, so the frontend needs the preset's product line to look
+    for that range's colours first."""
+    response = await async_client.get("/api/v1/inventory/colors/map")
+
+    presets = response.json()["preset_materials"]
+    assert presets["GFA00"] == "PLA Basic"
+    assert presets["GFA01"] == "PLA Matte"
+    assert all(not name.startswith("Bambu ") for name in presets.values())

+ 29 - 0
frontend/src/__tests__/contexts/ColorCatalogContext.test.tsx

@@ -81,6 +81,35 @@ describe('ColorCatalogProvider', () => {
     expect(getColorName('8344b0')).toBe('Purple');
     expect(getColorName('8344b0')).toBe('Purple');
   });
   });
 
 
+  it('carries the Bambu colours through for nearest naming (#3279)', async () => {
+    server.use(
+      http.get('/api/v1/inventory/colors/map', () =>
+        HttpResponse.json({
+          colors: {},
+          by_material: {},
+          bambu: [
+            { hex: '5b6579', name: 'Blue Grey', material: 'PLA Basic' },
+            { hex: 'ff9016', name: 'Pumpkin Orange', material: 'PLA Basic' },
+            { hex: 'ff911a', name: 'Translucent Orange', material: 'PETG Translucent' },
+          ],
+          preset_materials: { GFG01: 'PETG Translucent' },
+        })
+      )
+    );
+
+    const Wrapper = createWrapper();
+    render(
+      <Wrapper>
+        <div data-testid="child">ok</div>
+      </Wrapper>
+    );
+
+    await waitFor(() => {
+      expect(getColorName('5A657BFF')).toBe('Blue Grey');
+    });
+    expect(getColorName('FF9017', '', 'GFG01')).toBe('Translucent Orange');
+  });
+
   it('carries the material-qualified names through to getColorName (#2875)', async () => {
   it('carries the material-qualified names through to getColorName (#2875)', async () => {
     server.use(
     server.use(
       http.get('/api/v1/inventory/colors/map', () =>
       http.get('/api/v1/inventory/colors/map', () =>

+ 29 - 2
frontend/src/__tests__/hooks/useFilamentMapping.test.ts

@@ -5,7 +5,7 @@
  * selected during slicing is used when multiple trays have identical filament.
  * selected during slicing is used when multiple trays have identical filament.
  */
  */
 
 
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, afterEach } from 'vitest';
 import { renderHook } from '@testing-library/react';
 import { renderHook } from '@testing-library/react';
 import {
 import {
   buildAmsMapping,
   buildAmsMapping,
@@ -15,7 +15,7 @@ import {
   useFilamentMapping,
   useFilamentMapping,
 } from '../../hooks/useFilamentMapping';
 } from '../../hooks/useFilamentMapping';
 import { effectivePreferLowest } from '../../utils/amsHelpers';
 import { effectivePreferLowest } from '../../utils/amsHelpers';
-import { getColorName } from '../../utils/colors';
+import { getColorName, setColorCatalog, __resetColorCatalogForTests } from '../../utils/colors';
 import type { PrinterStatus } from '../../api/client';
 import type { PrinterStatus } from '../../api/client';
 
 
 // Helper to create a minimal printer status with AMS data
 // Helper to create a minimal printer status with AMS data
@@ -1624,3 +1624,30 @@ describe('buildLoadedFilaments — naming a slot after its bound spool', () => {
     expect(slot.colorName).toBe('Orange');
     expect(slot.colorName).toBe('Orange');
   });
   });
 });
 });
+
+describe('buildLoadedFilaments — a slot set on the printer screen (#3279)', () => {
+  afterEach(() => {
+    __resetColorCatalogForTests();
+  });
+
+  it("names the slot from its preset's range when the printer sends no sub-brand", () => {
+    __resetColorCatalogForTests();
+    setColorCatalog(
+      {},
+      {},
+      [
+        { hex: 'ff9016', name: 'Pumpkin Orange', material: 'PLA Basic' },
+        { hex: 'ff911a', name: 'Translucent Orange', material: 'PETG Translucent' },
+      ],
+      { GFG01: 'PETG Translucent' },
+    );
+    const status = createPrinterStatus(
+      [{ id: 0, tray: [{ id: 0, tray_type: 'PETG', tray_color: 'FF9017FF', tray_info_idx: 'GFG01' }] }],
+      [{ id: 254, tray_type: 'PETG', tray_color: 'FF9017FF', tray_info_idx: 'GFG01' }],
+    );
+
+    const names = buildLoadedFilaments(status).map((f) => f.colorName);
+
+    expect(names).toEqual(['Translucent Orange', 'Translucent Orange']);
+  });
+});

+ 122 - 0
frontend/src/__tests__/pages/PrintersPageColorName3279.test.tsx

@@ -0,0 +1,122 @@
+/**
+ * The AMS hover card names a colour picked on the printer's screen (#3279).
+ *
+ * Without RFID the printer reports a palette hex a step off Bambu Studio's and
+ * leaves tray_sub_brands empty. The card passes the slot's preset id, so the
+ * colour is looked up in that preset's range: Translucent Orange, not the PLA
+ * Basic Pumpkin Orange that is nearer across all ranges.
+ */
+
+import { describe, it, expect, afterEach } from 'vitest';
+import { screen, waitFor, fireEvent } from '@testing-library/react';
+import { render } from '../utils';
+import { PrintersPage } from '../../pages/PrintersPage';
+import { http, HttpResponse } from 'msw';
+import { server } from '../mocks/server';
+import { setColorCatalog, __resetColorCatalogForTests } from '../../utils/colors';
+
+const mockPrinter = {
+  id: 1,
+  name: 'X1 Carbon',
+  ip_address: '192.168.1.100',
+  serial_number: '00M09A350100001',
+  access_code: '12345678',
+  model: 'X1C',
+  enabled: true,
+  nozzle_diameter: 0.4,
+  nozzle_type: 'hardened_steel',
+  location: 'Workshop',
+  auto_archive: true,
+  created_at: '2024-01-01T00:00:00Z',
+  updated_at: '2024-01-01T00:00:00Z',
+};
+
+/** PETG Translucent orange picked on the printer's screen: no RFID, so no
+ *  sub-brand and no colour code, only the preset and a palette hex. */
+const screenPicked = {
+  tray_color: 'FF9017FF',
+  tray_type: 'PETG',
+  tray_sub_brands: '',
+  tray_id_name: '',
+  tray_info_idx: 'GFG01',
+  remain: 80,
+  k: 0.02,
+  cali_idx: null,
+  tag_uid: null,
+  tray_uuid: null,
+  nozzle_temp_min: 190,
+  nozzle_temp_max: 230,
+  drying_temp: null,
+  drying_time: null,
+  state: 11,
+};
+
+const status = {
+  connected: true,
+  state: 'IDLE',
+  progress: 0,
+  layer_num: 0,
+  total_layers: 0,
+  temperatures: { nozzle: 25, bed: 25, chamber: 25 },
+  remaining_time: 0,
+  filename: null,
+  wifi_signal: -50,
+  speed_level: 2,
+  vt_tray: [],
+  ams: [
+    {
+      id: 0,
+      humidity: 30,
+      temp: 25,
+      is_ams_ht: false,
+      serial_number: 'AMS00',
+      sw_ver: '1.0.0',
+      dry_time: 0,
+      dry_status: 0,
+      dry_sub_status: 0,
+      dry_sf_reason: [],
+      module_type: 'ams',
+      tray: [{ id: 0, ...screenPicked }],
+    },
+  ],
+};
+
+/** Hover-card visibility flips after an 80ms timeout — wait it out. */
+async function hoverFirstSlot() {
+  await waitFor(() => {
+    expect(screen.getAllByTestId('filament-slot').length).toBeGreaterThan(0);
+  });
+  fireEvent.mouseEnter(screen.getAllByTestId('filament-slot')[0]);
+}
+
+describe('PrintersPage — colour picked on the printer screen (#3279)', () => {
+  afterEach(() => {
+    __resetColorCatalogForTests();
+  });
+
+  it("names it from the slot preset's range", async () => {
+    setColorCatalog(
+      {},
+      {},
+      [
+        { hex: 'ff9016', name: 'Pumpkin Orange', material: 'PLA Basic' },
+        { hex: 'ff911a', name: 'Translucent Orange', material: 'PETG Translucent' },
+      ],
+      { GFG01: 'PETG Translucent' },
+    );
+    server.use(
+      http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])),
+      http.get('/api/v1/queue/', () => HttpResponse.json([])),
+      http.get('/api/v1/printers/:id/status', () => HttpResponse.json(status)),
+      http.get('/api/v1/printers/:id/slot-presets', () => HttpResponse.json({})),
+    );
+
+    render(<PrintersPage />);
+    await hoverFirstSlot();
+
+    await waitFor(() => {
+      expect(screen.getByText('Translucent Orange')).toBeInTheDocument();
+    });
+    expect(screen.queryByText('Pumpkin Orange')).not.toBeInTheDocument();
+  });
+});

+ 113 - 0
frontend/src/__tests__/utils/colors.test.ts

@@ -156,6 +156,119 @@ describe('getColorName with a material (#2875)', () => {
   });
   });
 });
 });
 
 
+describe('getColorName: nearest Bambu colour (#3279)', () => {
+  // A colour picked on the printer's touchscreen arrives as a hex a step or
+  // two off Bambu Studio's: Blue Grey is 5B6579 in the catalogue, the printer
+  // reports 5A657B. The exact lookup missed it and the hue bucket said "Blue".
+  const BAMBU = [
+    { hex: '5b6579', name: 'Blue Grey', material: 'PLA Basic' },
+    { hex: 'ff9016', name: 'Pumpkin Orange', material: 'PLA Basic' },
+    { hex: 'ff911a', name: 'Translucent Orange', material: 'PETG Translucent' },
+    { hex: 'a6a9aa', name: 'Silver', material: 'PLA Basic' },
+  ];
+
+  beforeEach(() => {
+    __resetColorCatalogForTests();
+    setColorCatalog({}, {}, BAMBU);
+  });
+
+  it('names a printer-palette hex by the Bambu colour it is a step away from', () => {
+    expect(getColorName('5A657BFF')).toBe('Blue Grey');
+    expect(getColorName('5A657B', 'PLA Basic')).toBe('Blue Grey');
+  });
+
+  it('keeps the hue bucket for a colour no Bambu entry is close to', () => {
+    expect(getColorName('123456')).toBe('Blue');
+  });
+
+  it('prefers the colour of the material the caller names', () => {
+    // FF9017 is nearer Pumpkin Orange (PLA Basic) than Translucent Orange (PETG).
+    expect(getColorName('FF9017')).toBe('Pumpkin Orange');
+    expect(getColorName('FF9017', 'PETG Translucent')).toBe('Translucent Orange');
+    expect(getColorName('FF9017', ' petg TRANSLUCENT ')).toBe('Translucent Orange');
+  });
+
+  it('looks in every Bambu range when the material is not one of them', () => {
+    expect(getColorName('5A657B', 'Generic PETG')).toBe('Blue Grey');
+  });
+
+  it('does not borrow another range\'s name when the material is a Bambu range', () => {
+    expect(getColorName('5A657B', 'PETG Translucent')).toBe('Blue');
+  });
+
+  it('never moves a different Bambu colour onto the file colour', () => {
+    // #3279's own pair: the file's A4AAAC is Silver, the tray's 5A657B Blue Grey.
+    expect(getColorName('A4AAAC')).toBe('Silver');
+  });
+
+  it('lets an exact catalogue name win over a nearer-sounding guess', () => {
+    __resetColorCatalogForTests();
+    setColorCatalog({ '5a657b': 'Slate' }, {}, BAMBU);
+    expect(getColorName('5A657B')).toBe('Slate');
+  });
+
+  it('forgets the old answers when the catalogue is replaced', () => {
+    expect(getColorName('5A657B')).toBe('Blue Grey');
+    setColorCatalog({}, {}, [{ hex: '5b6579', name: 'Renamed Grey', material: 'PLA Basic' }]);
+    expect(getColorName('5A657B')).toBe('Renamed Grey');
+  });
+
+  it('takes the first listed of two equally near colours', () => {
+    __resetColorCatalogForTests();
+    setColorCatalog({}, {}, [
+      { hex: '5b6579', name: 'Blue Grey', material: 'PLA Basic' },
+      { hex: '5b6579', name: 'Slate Grey', material: 'PLA Matte' },
+    ]);
+    expect(getColorName('5A657B')).toBe('Blue Grey');
+  });
+
+  it('stays within the material when it has Bambu colours of its own', () => {
+    // A PLA Basic slot at 8F8F8F is not Translucent Gray (PETG Translucent,
+    // 0.3 away) just because PLA Basic's Gray is further than the threshold.
+    __resetColorCatalogForTests();
+    setColorCatalog({}, {}, [
+      { hex: '8e8e8e', name: 'Translucent Gray', material: 'PETG Translucent' },
+      { hex: '8e9089', name: 'Gray', material: 'PLA Basic' },
+    ]);
+    expect(getColorName('8F8F8F', 'PLA Basic')).toBe('Gray');
+    expect(getColorName('8F8F8F', 'PLA Basic')).not.toBe('Translucent Gray');
+    expect(getColorName('8F8F8F', 'Some Third Party PLA')).toBe('Translucent Gray');
+  });
+
+  it("finds the material's own colour before another range's exact hex", () => {
+    // Pumpkin Orange (PLA Basic) owns FF9016 in the flat map; a PETG
+    // Translucent slot one step away from it is still Translucent Orange.
+    __resetColorCatalogForTests();
+    setColorCatalog({ ff9016: 'Pumpkin Orange' }, {}, BAMBU);
+    expect(getColorName('FF9016', 'PETG Translucent')).toBe('Translucent Orange');
+    expect(getColorName('FF9016')).toBe('Pumpkin Orange');
+  });
+
+  it("takes the material from the slot's preset when the printer sends none", () => {
+    // Without RFID the printer leaves tray_sub_brands empty but still reports
+    // the preset picked on its screen (GFA00 = Bambu PLA Basic).
+    __resetColorCatalogForTests();
+    setColorCatalog({}, {}, BAMBU, { GFA00: 'PLA Basic', GFG01: 'PETG Translucent' });
+    expect(getColorName('FF9017', '', 'GFG01')).toBe('Translucent Orange');
+    expect(getColorName('FF9017', null, 'gfa00')).toBe('Pumpkin Orange');
+    // tray_sub_brands, when there is one, is the more direct answer.
+    expect(getColorName('FF9017', 'PLA Basic', 'GFG01')).toBe('Pumpkin Orange');
+  });
+
+  it('keeps Clear for a transparent spool', () => {
+    expect(getColorName('5A657B00')).toBe('Clear');
+  });
+
+  it('ignores malformed entries', () => {
+    __resetColorCatalogForTests();
+    setColorCatalog({}, {}, [
+      { hex: 'zzzzzz', name: 'Nope', material: 'PLA Basic' },
+      { hex: '5b6579', name: '', material: 'PLA Basic' },
+    ]);
+    expect(getColorName('5A657B')).toBe('Blue');
+  });
+});
+
 describe('resolveSpoolColorName', () => {
 describe('resolveSpoolColorName', () => {
   beforeEach(() => {
   beforeEach(() => {
     __resetColorCatalogForTests();
     __resetColorCatalogForTests();

+ 7 - 1
frontend/src/api/client.ts

@@ -1,4 +1,5 @@
 import type { KProfileWriteResult } from '../utils/kprofileWrite';
 import type { KProfileWriteResult } from '../utils/kprofileWrite';
+import type { BambuCatalogColor } from '../utils/colors';
 import type { ArchivePlatesResponse, LibraryFilePlatesResponse } from '../types/plates';
 import type { ArchivePlatesResponse, LibraryFilePlatesResponse } from '../types/plates';
 import { batched, byField, byId } from './batch';
 import { batched, byField, byId } from './batch';
 
 
@@ -7175,7 +7176,12 @@ export const api = {
    *  flat answer (e.g. ``"pla matte|ffffff" -> "Ivory White"`` where the flat
    *  flat answer (e.g. ``"pla matte|ffffff" -> "Ivory White"`` where the flat
    *  map says "Jade White"). #2875. */
    *  map says "Jade White"). #2875. */
   getColorNameMap: () =>
   getColorNameMap: () =>
-    request<{ colors: Record<string, string>; by_material?: Record<string, string> }>('/inventory/colors/map'),
+    request<{
+      colors: Record<string, string>;
+      by_material?: Record<string, string>;
+      bambu?: BambuCatalogColor[];
+      preset_materials?: Record<string, string>;
+    }>('/inventory/colors/map'),
   addColorEntry: (data: {
   addColorEntry: (data: {
     manufacturer: string;
     manufacturer: string;
     color_name: string;
     color_name: string;

+ 1 - 1
frontend/src/components/PrintModal/FilamentOverride.tsx

@@ -153,7 +153,7 @@ export function FilamentOverride({
                     // keeps the sub-brand and the index as it did before.
                     // keeps the sub-brand and the index as it did before.
                     value: `${f.type}|${f.color}`,
                     value: `${f.type}|${f.color}`,
                     key: `${f.type}-${f.color}-${f.tray_sub_brands}-${idx}`,
                     key: `${f.type}-${f.color}-${f.tray_sub_brands}-${idx}`,
-                    label: `${f.tray_sub_brands || f.type} (${getColorName(f.color, f.tray_sub_brands)})`,
+                    label: `${f.tray_sub_brands || f.type} (${getColorName(f.color, f.tray_sub_brands, f.tray_info_idx)})`,
                     rgba: f.color,
                     rgba: f.color,
                     hex: displayHex(f.color),
                     hex: displayHex(f.color),
                     exactMatch:
                     exactMatch:

+ 7 - 2
frontend/src/contexts/ColorCatalogContext.tsx

@@ -26,7 +26,12 @@ export function ColorCatalogProvider({ children }: { children: ReactNode }) {
     queryKey: ['color-catalog-map'],
     queryKey: ['color-catalog-map'],
     queryFn: async () => {
     queryFn: async () => {
       const response = await api.getColorNameMap();
       const response = await api.getColorNameMap();
-      return { colors: response.colors, byMaterial: response.by_material ?? {} };
+      return {
+        colors: response.colors,
+        byMaterial: response.by_material ?? {},
+        bambu: response.bambu ?? [],
+        presetMaterials: response.preset_materials ?? {},
+      };
     },
     },
     // Catalog rarely changes during a session; no background refetch needed.
     // Catalog rarely changes during a session; no background refetch needed.
     staleTime: Infinity,
     staleTime: Infinity,
@@ -37,7 +42,7 @@ export function ColorCatalogProvider({ children }: { children: ReactNode }) {
 
 
   useEffect(() => {
   useEffect(() => {
     if (data) {
     if (data) {
-      setColorCatalog(data.colors, data.byMaterial);
+      setColorCatalog(data.colors, data.byMaterial, data.bambu, data.presetMaterials);
     }
     }
   }, [data]);
   }, [data]);
 
 

+ 2 - 2
frontend/src/hooks/useFilamentMapping.ts

@@ -76,7 +76,7 @@ export function buildLoadedFilaments(
         filaments.push({
         filaments.push({
           type: tray.tray_type,
           type: tray.tray_type,
           color,
           color,
-          colorName: identity?.color_name?.trim() || getColorName(color, tray.tray_sub_brands),
+          colorName: identity?.color_name?.trim() || getColorName(color, tray.tray_sub_brands, tray.tray_info_idx),
           amsId: amsUnit.id,
           amsId: amsUnit.id,
           trayId: tray.id,
           trayId: tray.id,
           isHt,
           isHt,
@@ -108,7 +108,7 @@ export function buildLoadedFilaments(
       filaments.push({
       filaments.push({
         type: extTray.tray_type,
         type: extTray.tray_type,
         color,
         color,
-        colorName: identity?.color_name?.trim() || getColorName(color, extTray.tray_sub_brands),
+        colorName: identity?.color_name?.trim() || getColorName(color, extTray.tray_sub_brands, extTray.tray_info_idx),
         amsId: -1,
         amsId: -1,
         trayId: trayId - 254,
         trayId: trayId - 254,
         isHt: false,
         isHt: false,

+ 3 - 3
frontend/src/pages/PrintersPage.tsx

@@ -5576,7 +5576,7 @@ function PrinterCard({
                                   // vendor-less form. Strip the "@<printer>..." suffix that
                                   // vendor-less form. Strip the "@<printer>..." suffix that
                                   // BambuStudio appends to user-preset names.
                                   // BambuStudio appends to user-preset names.
                                   profile: slotPresetName || (slotSpoolForFill ? [slotSpoolForFill.brand, slotSpoolForFill.slicer_filament_name?.split('@')[0].trim() || slotSpoolForFill.material].filter(Boolean).join(' ').trim() : null) || inventoryAssignment?.spool?.slicer_filament_name || cloudInfo?.name || tray.tray_sub_brands || tray.tray_type,
                                   profile: slotPresetName || (slotSpoolForFill ? [slotSpoolForFill.brand, slotSpoolForFill.slicer_filament_name?.split('@')[0].trim() || slotSpoolForFill.material].filter(Boolean).join(' ').trim() : null) || inventoryAssignment?.spool?.slicer_filament_name || cloudInfo?.name || tray.tray_sub_brands || tray.tray_type,
-                                  colorName: getColorName(tray.tray_color || '', tray.tray_sub_brands),
+                                  colorName: getColorName(tray.tray_color || '', tray.tray_sub_brands, tray.tray_info_idx),
                                   colorHex: tray.tray_color || null,
                                   colorHex: tray.tray_color || null,
                                   kFactor: formatKValue(tray.k),
                                   kFactor: formatKValue(tray.k),
                                   fillLevel: effectiveFill,
                                   fillLevel: effectiveFill,
@@ -5866,7 +5866,7 @@ function PrinterCard({
                         const filamentData = tray?.tray_type ? {
                         const filamentData = tray?.tray_type ? {
                           vendor: (isBambuLabSpool(tray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
                           vendor: (isBambuLabSpool(tray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
                           profile: slotPresetName || (htSlotSpoolForFill ? [htSlotSpoolForFill.brand, htSlotSpoolForFill.slicer_filament_name?.split('@')[0].trim() || htSlotSpoolForFill.material].filter(Boolean).join(' ').trim() : null) || htInventoryAssignment?.spool?.slicer_filament_name || cloudInfo?.name || tray.tray_sub_brands || tray.tray_type,
                           profile: slotPresetName || (htSlotSpoolForFill ? [htSlotSpoolForFill.brand, htSlotSpoolForFill.slicer_filament_name?.split('@')[0].trim() || htSlotSpoolForFill.material].filter(Boolean).join(' ').trim() : null) || htInventoryAssignment?.spool?.slicer_filament_name || cloudInfo?.name || tray.tray_sub_brands || tray.tray_type,
-                          colorName: getColorName(tray.tray_color || '', tray.tray_sub_brands),
+                          colorName: getColorName(tray.tray_color || '', tray.tray_sub_brands, tray.tray_info_idx),
                           colorHex: tray.tray_color || null,
                           colorHex: tray.tray_color || null,
                           kFactor: formatKValue(tray.k),
                           kFactor: formatKValue(tray.k),
                           fillLevel: htEffectiveFill,
                           fillLevel: htEffectiveFill,
@@ -6301,7 +6301,7 @@ function PrinterCard({
                               const extFilamentData = {
                               const extFilamentData = {
                                 vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
                                 vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
                                 profile: extSlotPresetName || (extSlotSpoolForFill ? [extSlotSpoolForFill.brand, extSlotSpoolForFill.slicer_filament_name?.split('@')[0].trim() || extSlotSpoolForFill.material].filter(Boolean).join(' ').trim() : null) || extInventoryAssignment?.spool?.slicer_filament_name || extCloudInfo?.name || extTray.tray_sub_brands || extTray.tray_type || 'Unknown',
                                 profile: extSlotPresetName || (extSlotSpoolForFill ? [extSlotSpoolForFill.brand, extSlotSpoolForFill.slicer_filament_name?.split('@')[0].trim() || extSlotSpoolForFill.material].filter(Boolean).join(' ').trim() : null) || extInventoryAssignment?.spool?.slicer_filament_name || extCloudInfo?.name || extTray.tray_sub_brands || extTray.tray_type || 'Unknown',
-                                colorName: getColorName(extTray.tray_color || '', extTray.tray_sub_brands),
+                                colorName: getColorName(extTray.tray_color || '', extTray.tray_sub_brands, extTray.tray_info_idx),
                                 colorHex: extTray.tray_color || null,
                                 colorHex: extTray.tray_color || null,
                                 kFactor: formatKValue(extTray.k),
                                 kFactor: formatKValue(extTray.k),
                                 fillLevel: extEffectiveFill,
                                 fillLevel: extEffectiveFill,

+ 86 - 4
frontend/src/utils/colors.ts

@@ -1,3 +1,5 @@
+import { colorDistance } from './amsHelpers';
+
 // Runtime color-name catalog, populated once at app startup by ColorCatalogProvider
 // Runtime color-name catalog, populated once at app startup by ColorCatalogProvider
 // from /api/inventory/colors/map. The backend color_catalog table is the single
 // from /api/inventory/colors/map. The backend color_catalog table is the single
 // source of truth — no hardcoded hex→name tables live on the frontend anymore.
 // source of truth — no hardcoded hex→name tables live on the frontend anymore.
@@ -16,10 +18,36 @@ let runtimeColorCatalog: Record<string, string> = {};
 // it as tray_sub_brands) gets the right one instead of whichever row the
 // it as tray_sub_brands) gets the right one instead of whichever row the
 // backend's collapse happened to keep (#2875).
 // backend's collapse happened to keep (#2875).
 let runtimeMaterialCatalog: Record<string, string> = {};
 let runtimeMaterialCatalog: Record<string, string> = {};
+// Every Bambu Lab catalogue colour, for naming a hex no exact lookup finds
+// (#3279). A colour picked on the printer's touchscreen is reported a step or
+// two off Bambu Studio's hex -- 5A657B for Blue Grey 5B6579 -- and fell through
+// to the hue bucket, which called it "Blue".
+export interface BambuCatalogColor {
+  hex: string;
+  name: string;
+  material: string;
+}
+let runtimeBambuColors: BambuCatalogColor[] = [];
+// Bambu preset id (upper case) to its range in lower case: GFA00 -> "pla basic".
+let runtimePresetMaterials: Record<string, string> = {};
+// Nearest-name answers by "<material>|<hex>", as each miss compares against
+// every Bambu colour. Emptied whenever the catalogue is replaced.
+let nearestNameCache = new Map<string, string | null>();
+// CIEDE2000 delta-E within which a hex is taken to be the catalogue colour.
+// Blue Grey's two hexes are 0.78 apart; the next Bambu colour from 5A657B is
+// 8.3 away. Different Bambu colours can sit closer than this across materials
+// (Pumpkin Orange / Translucent Orange: 0.26), which is why the caller's
+// material is searched first.
+const NEAREST_NAME_MAX_DELTA_E = 3;
 let catalogVersion = 0;
 let catalogVersion = 0;
 const catalogListeners = new Set<() => void>();
 const catalogListeners = new Set<() => void>();
 
 
-export function setColorCatalog(map: Record<string, string>, byMaterial?: Record<string, string>): void {
+export function setColorCatalog(
+  map: Record<string, string>,
+  byMaterial?: Record<string, string>,
+  bambu?: BambuCatalogColor[],
+  presetMaterials?: Record<string, string>,
+): void {
   // Normalize keys to lowercase 6-char hex (no '#'), defensively. Backend already
   // Normalize keys to lowercase 6-char hex (no '#'), defensively. Backend already
   // does this, but the frontend contract is explicit so callers from tests or
   // does this, but the frontend contract is explicit so callers from tests or
   // future integrations can't accidentally break lookups.
   // future integrations can't accidentally break lookups.
@@ -42,6 +70,19 @@ export function setColorCatalog(map: Record<string, string>, byMaterial?: Record
   }
   }
   runtimeColorCatalog = normalized;
   runtimeColorCatalog = normalized;
   runtimeMaterialCatalog = normalizedByMaterial;
   runtimeMaterialCatalog = normalizedByMaterial;
+  runtimeBambuColors = (bambu || [])
+    .map((entry) => ({
+      hex: (entry?.hex || '').replace('#', '').toLowerCase().slice(0, 6),
+      name: entry?.name || '',
+      material: (entry?.material || '').trim().toLowerCase(),
+    }))
+    .filter((entry) => /^[0-9a-f]{6}$/.test(entry.hex) && entry.name);
+  const normalizedPresets: Record<string, string> = {};
+  for (const [preset, range] of Object.entries(presetMaterials || {})) {
+    if (preset && range) normalizedPresets[preset.trim().toUpperCase()] = range.trim().toLowerCase();
+  }
+  runtimePresetMaterials = normalizedPresets;
+  nearestNameCache = new Map();
   catalogVersion += 1;
   catalogVersion += 1;
   // Snapshot listeners to avoid mutation-during-iteration if a listener unsubscribes.
   // Snapshot listeners to avoid mutation-during-iteration if a listener unsubscribes.
   for (const listener of Array.from(catalogListeners)) {
   for (const listener of Array.from(catalogListeners)) {
@@ -64,6 +105,9 @@ export function getColorCatalogVersion(): number {
 export function __resetColorCatalogForTests(): void {
 export function __resetColorCatalogForTests(): void {
   runtimeColorCatalog = {};
   runtimeColorCatalog = {};
   runtimeMaterialCatalog = {};
   runtimeMaterialCatalog = {};
+  runtimeBambuColors = [];
+  runtimePresetMaterials = {};
+  nearestNameCache = new Map();
   catalogVersion = 0;
   catalogVersion = 0;
   catalogListeners.clear();
   catalogListeners.clear();
 }
 }
@@ -237,20 +281,58 @@ export function colorSortKey(rgba: string | null | undefined): string {
  * An unknown material falls through to the flat lookup, so passing one can only
  * An unknown material falls through to the flat lookup, so passing one can only
  * ever improve the answer.
  * ever improve the answer.
  */
  */
-export function getColorName(hexColor: string, material?: string | null): string {
+export function getColorName(hexColor: string, material?: string | null, trayInfoIdx?: string | null): string {
   if (!hexColor) return hexToColorName(hexColor);
   if (!hexColor) return hexToColorName(hexColor);
   const clean = hexColor.replace('#', '').toLowerCase();
   const clean = hexColor.replace('#', '').toLowerCase();
   if (clean.length === 8 && clean.substring(6, 8) === '00') return 'Clear';
   if (clean.length === 8 && clean.substring(6, 8) === '00') return 'Clear';
   const hex = clean.substring(0, 6);
   const hex = clean.substring(0, 6);
-  if (material) {
-    const qualified = runtimeMaterialCatalog[`${material.trim().toLowerCase()}|${hex}`];
+  // Without RFID the printer sends no tray_sub_brands, but it does report the
+  // preset picked on its screen, and a Bambu preset names its range (#3279).
+  const range = (material || '').trim().toLowerCase() || runtimePresetMaterials[(trayInfoIdx || '').toUpperCase()] || '';
+  if (range) {
+    const qualified = runtimeMaterialCatalog[`${range}|${hex}`];
     if (qualified) return qualified;
     if (qualified) return qualified;
   }
   }
+  const rangeColors = range ? runtimeBambuColors.filter((entry) => entry.material === range) : [];
+  if (rangeColors.length > 0) {
+    const inRange = nearestBambuColorName(hex, range, rangeColors);
+    if (inRange) return inRange;
+  }
   const mapped = runtimeColorCatalog[hex];
   const mapped = runtimeColorCatalog[hex];
   if (mapped) return mapped;
   if (mapped) return mapped;
+  // Other ranges only for a slot whose own range has no Bambu colours: a PLA
+  // Basic grey is not Translucent Gray because PLA Basic's Gray is further off.
+  if (rangeColors.length === 0) {
+    const anyRange = nearestBambuColorName(hex, '', runtimeBambuColors);
+    if (anyRange) return anyRange;
+  }
   return hexToColorName(hexColor);
   return hexToColorName(hexColor);
 }
 }
 
 
+/**
+ * The colour in ``entries`` within ``NEAREST_NAME_MAX_DELTA_E`` of ``hex``,
+ * or null (#3279). Nearest wins, the first listed on a tie. ``range`` keys
+ * the cache: the same hex has a different answer in each range.
+ */
+function nearestBambuColorName(hex: string, range: string, entries: BambuCatalogColor[]): string | null {
+  const cacheKey = `${range}|${hex}`;
+  const cached = nearestNameCache.get(cacheKey);
+  if (cached !== undefined) return cached;
+
+  let best: string | null = null;
+  let bestDistance = Infinity;
+  for (const entry of entries) {
+    const distance = colorDistance(hex, entry.hex);
+    if (distance !== null && distance < bestDistance) {
+      best = entry.name;
+      bestDistance = distance;
+    }
+  }
+  const name = bestDistance <= NEAREST_NAME_MAX_DELTA_E ? best : null;
+  nearestNameCache.set(cacheKey, name);
+  return name;
+}
+
 /**
 /**
  * Label two colours so a reader can tell which is which.
  * Label two colours so a reader can tell which is which.
  *
  *

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 1 - 1
static/assets/inventorySearch-DLr8RqvF.js → static/assets/inventorySearch-pLpY8YP6.js

@@ -1 +1 @@
-import{c as e}from"./colors-je4aRt_-.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};
+import{c as e}from"./colors-BF4_h-XA.js";function t(t,n){let r=n.trim().toLowerCase();if(!r)return!0;let i=r.match(/^#(\d+)$/);return i?t.id===Number(i[1]):String(t.id).includes(r)||t.material.toLowerCase().includes(r)||(t.brand?.toLowerCase().includes(r)??!1)||(t.color_name?.toLowerCase().includes(r)??!1)||(e(t.color_name,t.rgba,t.color_name_is_synthesized)?.toLowerCase().includes(r)??!1)||(t.subtype?.toLowerCase().includes(r)??!1)||(t.note?.toLowerCase().includes(r)??!1)||(t.slicer_filament_name?.toLowerCase().includes(r)??!1)||(t.storage_location?.toLowerCase().includes(r)??!1)||(t.material_number?.toLowerCase().includes(r)??!1)||(t.suppliers?.some(e=>e.supplier_name.toLowerCase().includes(r)||(e.supplier_article_number?.toLowerCase().includes(r)??!1))??!1)}function n(e,n){return n?e.filter(e=>t(e,n)):e}export{n as t};

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


+ 1 - 0
static/assets/useColorCatalogVersion-7yzLvEqw.js

@@ -0,0 +1 @@
+import{i as e}from"./rolldown-runtime-aKtaBQYM.js";import{k as t}from"./client-DSw2TdH4.js";import{d as n,r}from"./colors-BF4_h-XA.js";var i=e(t(),1);function a(){return(0,i.useSyncExternalStore)(n,r,r)}export{a as t};

+ 0 - 1
static/assets/useColorCatalogVersion-uixz-m0E.js

@@ -1 +0,0 @@
-import{i as e}from"./rolldown-runtime-aKtaBQYM.js";import{k as t}from"./client-DSw2TdH4.js";import{d as n,r}from"./colors-je4aRt_-.js";var i=e(t(),1);function a(){return(0,i.useSyncExternalStore)(n,r,r)}export{a as t};

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


+ 8 - 8
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-BNCUzO2G.js"></script>
+    <script type="module" crossorigin src="/assets/index-WCWK8Sj1.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-DSw2TdH4.js">
     <link rel="modulepreload" crossorigin href="/assets/client-DSw2TdH4.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-PmRWqOqI.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-PmRWqOqI.js">
@@ -88,16 +88,16 @@
     <link rel="modulepreload" crossorigin href="/assets/Card-BT_YWvIC.js">
     <link rel="modulepreload" crossorigin href="/assets/Card-BT_YWvIC.js">
     <link rel="modulepreload" crossorigin href="/assets/Button-C_-d0X5W.js">
     <link rel="modulepreload" crossorigin href="/assets/Button-C_-d0X5W.js">
     <link rel="modulepreload" crossorigin href="/assets/ConfirmModal-lZYjzFL6.js">
     <link rel="modulepreload" crossorigin href="/assets/ConfirmModal-lZYjzFL6.js">
-    <link rel="modulepreload" crossorigin href="/assets/colors-je4aRt_-.js">
-    <link rel="modulepreload" crossorigin href="/assets/currency-DS4qMz4d.js">
     <link rel="modulepreload" crossorigin href="/assets/date-G9KUauqz.js">
     <link rel="modulepreload" crossorigin href="/assets/date-G9KUauqz.js">
-    <link rel="modulepreload" crossorigin href="/assets/amsHelpers-CWcmx9Ax.js">
+    <link rel="modulepreload" crossorigin href="/assets/amsHelpers-Dlwij40i.js">
+    <link rel="modulepreload" crossorigin href="/assets/colors-BF4_h-XA.js">
+    <link rel="modulepreload" crossorigin href="/assets/currency-DS4qMz4d.js">
     <link rel="modulepreload" crossorigin href="/assets/useUiSettings-BoONraTd.js">
     <link rel="modulepreload" crossorigin href="/assets/useUiSettings-BoONraTd.js">
     <link rel="modulepreload" crossorigin href="/assets/NumberInput-BhsGuNcX.js">
     <link rel="modulepreload" crossorigin href="/assets/NumberInput-BhsGuNcX.js">
     <link rel="modulepreload" crossorigin href="/assets/printer-qo845RQA.js">
     <link rel="modulepreload" crossorigin href="/assets/printer-qo845RQA.js">
     <link rel="modulepreload" crossorigin href="/assets/filamentSwatchHelpers-C3r5m07g.js">
     <link rel="modulepreload" crossorigin href="/assets/filamentSwatchHelpers-C3r5m07g.js">
     <link rel="modulepreload" crossorigin href="/assets/FilamentSwatch-CElzyuMD.js">
     <link rel="modulepreload" crossorigin href="/assets/FilamentSwatch-CElzyuMD.js">
-    <link rel="modulepreload" crossorigin href="/assets/PrintModal-DJVIzPGw.js">
+    <link rel="modulepreload" crossorigin href="/assets/PrintModal-BtM1dElr.js">
     <link rel="modulepreload" crossorigin href="/assets/download-nyR-1mBX.js">
     <link rel="modulepreload" crossorigin href="/assets/download-nyR-1mBX.js">
     <link rel="modulepreload" crossorigin href="/assets/eye-c0u5gXED.js">
     <link rel="modulepreload" crossorigin href="/assets/eye-c0u5gXED.js">
     <link rel="modulepreload" crossorigin href="/assets/folder-kanban-Br4ECQKC.js">
     <link rel="modulepreload" crossorigin href="/assets/folder-kanban-Br4ECQKC.js">
@@ -108,7 +108,7 @@
     <link rel="modulepreload" crossorigin href="/assets/trash-2-CUF18zKt.js">
     <link rel="modulepreload" crossorigin href="/assets/trash-2-CUF18zKt.js">
     <link rel="modulepreload" crossorigin href="/assets/weight-DEayKDXK.js">
     <link rel="modulepreload" crossorigin href="/assets/weight-DEayKDXK.js">
     <link rel="modulepreload" crossorigin href="/assets/projectTree-C5LrJHZP.js">
     <link rel="modulepreload" crossorigin href="/assets/projectTree-C5LrJHZP.js">
-    <link rel="modulepreload" crossorigin href="/assets/ConfirmOutcomeDialog-B-f6oDOv.js">
+    <link rel="modulepreload" crossorigin href="/assets/ConfirmOutcomeDialog-ct-PiJ6A.js">
     <link rel="modulepreload" crossorigin href="/assets/key-DdGCOutm.js">
     <link rel="modulepreload" crossorigin href="/assets/key-DdGCOutm.js">
     <link rel="modulepreload" crossorigin href="/assets/log-out-CDt27Bxj.js">
     <link rel="modulepreload" crossorigin href="/assets/log-out-CDt27Bxj.js">
     <link rel="modulepreload" crossorigin href="/assets/shield-alert-Cjpp4ey-.js">
     <link rel="modulepreload" crossorigin href="/assets/shield-alert-Cjpp4ey-.js">
@@ -116,11 +116,11 @@
     <link rel="modulepreload" crossorigin href="/assets/upload-Dpjdym4t.js">
     <link rel="modulepreload" crossorigin href="/assets/upload-Dpjdym4t.js">
     <link rel="modulepreload" crossorigin href="/assets/wifi-off-2Cy9fC-p.js">
     <link rel="modulepreload" crossorigin href="/assets/wifi-off-2Cy9fC-p.js">
     <link rel="modulepreload" crossorigin href="/assets/ThemeContext-DGVIadPK.js">
     <link rel="modulepreload" crossorigin href="/assets/ThemeContext-DGVIadPK.js">
-    <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-uixz-m0E.js">
+    <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-7yzLvEqw.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-CpiTIx_j.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-CpiTIx_j.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-jD6ObGRm.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-jD6ObGRm.js">
     <link rel="modulepreload" crossorigin href="/assets/plateCheck-fqh7_usj.js">
     <link rel="modulepreload" crossorigin href="/assets/plateCheck-fqh7_usj.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-ONJVQ_oD.js">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-GVz6JdbD.js">
     <link rel="stylesheet" crossorigin href="/assets/index-Cs9GKX3n.css">
     <link rel="stylesheet" crossorigin href="/assets/index-Cs9GKX3n.css">
   </head>
   </head>
   <body>
   <body>

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