Jelajahi Sumber

feat(slicer): filter slice profiles by printer + default from the 3MF (issue #1325)

  The Slice dialog listed every process / filament preset regardless of
  the chosen printer, and always defaulted to the first listed preset
  rather than what the 3MF was prepared with (#1325).
  Matching uses the slicer's own compatible_printers list for imported
  (local) presets and falls back to the "@BBL <model>" name suffix for
  cloud / standard presets. Compatibility-unknown presets are never
  hidden.

  Defaults: the printer and process dropdowns default to the preset
  names embedded in the source 3MF's project_settings.config when those
  presets are available; the per-slot filament and process pre-picks
  prefer a printer-compatible preset, and switching the printer re-picks
  any selection left incompatible.

  - UnifiedPreset gains compatible_printers, exposed for the local tier
  - plates endpoints return embedded_printer / embedded_process
  - new frontend util slicerPrinterMatch.ts; extract_embedded_presets_from_3mf
  - slice.otherPrinters added across all 9 locales
maziggy 6 hari lalu
induk
melakukan
e738645b0d

+ 1 - 1
CHANGELOG.md

@@ -5,7 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.2.5b1] - Unreleased
 ## [0.2.5b1] - Unreleased
 
 
 ### Added
 ### Added
-- **Slicer: process & filament profiles filtered by the selected printer (#1325, requested by @IndividualGhost1905)** — In the server-side Slice dialog, picking a printer profile now filters the Process and Filament dropdowns to presets compatible with that printer; presets that resolve to a different Bambu model drop into a trailing "Other printers" group instead of cluttering the main list. Matching uses the slicer's own `compatible_printers` list for imported (local) presets, and falls back to the `@BBL <model>` name suffix for cloud and standard presets, so all three tiers are covered. Compatibility-unknown presets (custom or untagged) are never hidden. Defaults follow suit — the pre-picked process and per-slot filament now prefer a printer-compatible preset, and switching the printer re-picks any selection left incompatible. New `frontend/src/utils/slicerPrinterMatch.ts` (11 unit tests); `UnifiedPreset` now carries `compatible_printers`, exposed for the local tier (`backend/app/api/routes/slicer_presets.py`). Parity green, build clean.
+- **Slicer: process & filament profiles filtered by the selected printer (#1325, requested by @IndividualGhost1905)** — In the server-side Slice dialog, picking a printer profile now filters the Process and Filament dropdowns to presets compatible with that printer; presets that resolve to a different Bambu model drop into a trailing "Other printers" group instead of cluttering the main list. Matching uses the slicer's own `compatible_printers` list for imported (local) presets, and falls back to the `@BBL <model>` name suffix for cloud and standard presets, so all three tiers are covered. Compatibility-unknown presets (custom or untagged) are never hidden. Defaults follow suit — the pre-picked process and per-slot filament now prefer a printer-compatible preset, and switching the printer re-picks any selection left incompatible. The printer and process dropdowns also default to the preset names embedded in the source 3MF's `project_settings.config` when those presets are available, instead of always taking the first listed preset. New `frontend/src/utils/slicerPrinterMatch.ts` (11 unit tests) and `extract_embedded_presets_from_3mf` (5 unit tests); `UnifiedPreset` now carries `compatible_printers`, exposed for the local tier (`backend/app/api/routes/slicer_presets.py`); the plates endpoints return `embedded_printer` / `embedded_process`. Parity green, build clean.
 - **Spanish (es) translation (#1243, requested by @MiguelAngelLV)** — Bambuddy now ships a full European Spanish locale. New `frontend/src/i18n/locales/es.ts` translates all 4899 keys with placeholders, plural forms, and inline markup preserved; registered in `frontend/src/i18n/index.ts` and selectable as "Español" in the language picker. The parity checker auto-discovers the file — `frontend/scripts/check-i18n-parity.mjs` gained an `ES_COGNATES` allow-list for genuine Spanish cognates and brand/format tokens. Brings the supported-language count to 9 (en / de / es / fr / it / ja / pt-BR / zh-CN / zh-TW). Parity green, frontend build clean.
 - **Spanish (es) translation (#1243, requested by @MiguelAngelLV)** — Bambuddy now ships a full European Spanish locale. New `frontend/src/i18n/locales/es.ts` translates all 4899 keys with placeholders, plural forms, and inline markup preserved; registered in `frontend/src/i18n/index.ts` and selectable as "Español" in the language picker. The parity checker auto-discovers the file — `frontend/scripts/check-i18n-parity.mjs` gained an `ES_COGNATES` allow-list for genuine Spanish cognates and brand/format tokens. Brings the supported-language count to 9 (en / de / es / fr / it / ja / pt-BR / zh-CN / zh-TW). Parity green, frontend build clean.
 - **Currency: Belize Dollars (BZD) added to the Settings → Cost currency dropdown (#1454, requested by @PLGuerraDesigns)** — Reporter accurately tracks 3D-printing filament costs in his local currency and BZD wasn't selectable, forcing a manual 2:1 mental conversion from USD. Added `BZD: 'BZ$'` to `frontend/src/utils/currency.ts` next to MXN (Americas dollar-prefix grouping); `getCurrencySymbol('BZD')` returns `'BZ$'` and the SUPPORTED_CURRENCIES list now has 30 entries. Unit test added in `frontend/src/__tests__/utils/currency.test.ts` covering the symbol lookup and presence in SUPPORTED_CURRENCIES; entry-count assertion bumped to 30 so any future additions/removals are caught immediately. 14 currency tests green; frontend build clean.
 - **Currency: Belize Dollars (BZD) added to the Settings → Cost currency dropdown (#1454, requested by @PLGuerraDesigns)** — Reporter accurately tracks 3D-printing filament costs in his local currency and BZD wasn't selectable, forcing a manual 2:1 mental conversion from USD. Added `BZD: 'BZ$'` to `frontend/src/utils/currency.ts` next to MXN (Americas dollar-prefix grouping); `getCurrencySymbol('BZD')` returns `'BZ$'` and the SUPPORTED_CURRENCIES list now has 30 entries. Unit test added in `frontend/src/__tests__/utils/currency.test.ts` covering the symbol lookup and presence in SUPPORTED_CURRENCIES; entry-count assertion bumped to 30 so any future additions/removals are caught immediately. 14 currency tests green; frontend build clean.
 - **Connection Diagnostic — self-service triage for "printer won't connect / won't print"** — A triage review of recently-closed issues found roughly a third were user-side setup errors (printer not in LAN developer mode, blocked ports, Docker bridge networking, wrong access code, printer on a different subnet), each costing a multi-round-trip "enable debug logging → build a support bundle → upload it" exchange. A new diagnostic (`backend/app/services/printer_diagnostic.py`) runs those checks automatically: TCP reachability of MQTT 8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network mode, printer/host subnet match, and MQTT credential class — each returning a pass / fail / warn / skip status with a localized plain-language fix. Exposed via `GET /printers/{id}/diagnostic` (saved printer) and `POST /printers/diagnostic` (pre-save Add-Printer flow), and surfaced as a one-click "Run diagnostic" from the printer card actions menu (plus a quick button on the card when a printer is offline), the Add-Printer dialog, and a new Connection Diagnostic section on the System page. The in-app bug reporter scans configured printers when the report form opens and always shows the result — a healthy confirmation when nothing's wrong, or the detected problem and its fix inline — so setup mistakes get self-resolved instead of becoming GitHub issues. The GitHub `config.yml` troubleshooting link was repointed from the wiki source repo to the rendered troubleshooting page. Backend service unit tests (15) and frontend modal tests (3) added; all diagnostic strings translated across the 8 locales. Backend ruff clean, frontend build clean, i18n parity green.
 - **Connection Diagnostic — self-service triage for "printer won't connect / won't print"** — A triage review of recently-closed issues found roughly a third were user-side setup errors (printer not in LAN developer mode, blocked ports, Docker bridge networking, wrong access code, printer on a different subnet), each costing a multi-round-trip "enable debug logging → build a support bundle → upload it" exchange. A new diagnostic (`backend/app/services/printer_diagnostic.py`) runs those checks automatically: TCP reachability of MQTT 8883 / FTPS 990 / RTSPS 322, LAN developer mode, Docker network mode, printer/host subnet match, and MQTT credential class — each returning a pass / fail / warn / skip status with a localized plain-language fix. Exposed via `GET /printers/{id}/diagnostic` (saved printer) and `POST /printers/diagnostic` (pre-save Add-Printer flow), and surfaced as a one-click "Run diagnostic" from the printer card actions menu (plus a quick button on the card when a printer is offline), the Add-Printer dialog, and a new Connection Diagnostic section on the System page. The in-app bug reporter scans configured printers when the report form opens and always shows the result — a healthy confirmation when nothing's wrong, or the detected problem and its fix inline — so setup mistakes get self-resolved instead of becoming GitHub issues. The GitHub `config.yml` troubleshooting link was repointed from the wiki source repo to the rendered troubleshooting page. Backend service unit tests (15) and frontend modal tests (3) added; all diagnostic strings translated across the 8 locales. Backend ruff clean, frontend build clean, i18n parity green.

+ 7 - 0
backend/app/api/routes/archives.py

@@ -31,6 +31,7 @@ from backend.app.schemas.slicer import SliceRequest
 from backend.app.services.archive import ArchiveService
 from backend.app.services.archive import ArchiveService
 from backend.app.utils.http import build_content_disposition
 from backend.app.utils.http import build_content_disposition
 from backend.app.utils.threemf_tools import (
 from backend.app.utils.threemf_tools import (
+    extract_embedded_presets_from_3mf,
     extract_nozzle_mapping_from_3mf,
     extract_nozzle_mapping_from_3mf,
     extract_project_filaments_from_3mf,
     extract_project_filaments_from_3mf,
 )
 )
@@ -3070,10 +3071,14 @@ async def get_archive_plates(
     # never raises NameError when the archive isn't a valid zip (e.g. plain
     # never raises NameError when the archive isn't a valid zip (e.g. plain
     # .gcode file from a sliced-archive flow that didn't request 3MF output).
     # .gcode file from a sliced-archive flow that didn't request 3MF output).
     gcode_files: list[str] = []
     gcode_files: list[str] = []
+    # Printer / process preset names the 3MF was prepared with — used by the
+    # SliceModal to default its dropdowns (#1325).
+    embedded_presets: dict[str, str | None] = {"printer": None, "process": None}
 
 
     try:
     try:
         with zipfile.ZipFile(file_path, "r") as zf:
         with zipfile.ZipFile(file_path, "r") as zf:
             namelist = zf.namelist()
             namelist = zf.namelist()
+            embedded_presets = extract_embedded_presets_from_3mf(zf)
 
 
             # Find all plate gcode files to determine available plates
             # Find all plate gcode files to determine available plates
             gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
             gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
@@ -3321,6 +3326,8 @@ async def get_archive_plates(
         "plates": plates,
         "plates": plates,
         "is_multi_plate": len(plates) > 1,
         "is_multi_plate": len(plates) > 1,
         "has_gcode": has_gcode,
         "has_gcode": has_gcode,
+        "embedded_printer": embedded_presets["printer"],
+        "embedded_process": embedded_presets["process"],
     }
     }
 
 
 
 

+ 8 - 0
backend/app/api/routes/library.py

@@ -65,6 +65,7 @@ from backend.app.schemas.slicer import SliceRequest, SliceResponse
 from backend.app.services.archive import ThreeMFParser
 from backend.app.services.archive import ThreeMFParser
 from backend.app.services.stl_thumbnail import generate_stl_thumbnail
 from backend.app.services.stl_thumbnail import generate_stl_thumbnail
 from backend.app.utils.threemf_tools import (
 from backend.app.utils.threemf_tools import (
+    extract_embedded_presets_from_3mf,
     extract_nozzle_mapping_from_3mf,
     extract_nozzle_mapping_from_3mf,
     extract_project_filaments_from_3mf,
     extract_project_filaments_from_3mf,
 )
 )
@@ -2193,10 +2194,15 @@ async def get_library_file_plates(
         return {"file_id": file_id, "filename": lib_file.filename, "plates": [], "is_multi_plate": False}
         return {"file_id": file_id, "filename": lib_file.filename, "plates": [], "is_multi_plate": False}
 
 
     plates = []
     plates = []
+    # Printer / process preset names the 3MF was prepared with — used by the
+    # SliceModal to default its dropdowns (#1325). Initialised here so the
+    # final return never raises NameError when the file isn't a valid zip.
+    embedded_presets: dict[str, str | None] = {"printer": None, "process": None}
 
 
     try:
     try:
         with zipfile.ZipFile(file_path, "r") as zf:
         with zipfile.ZipFile(file_path, "r") as zf:
             namelist = zf.namelist()
             namelist = zf.namelist()
+            embedded_presets = extract_embedded_presets_from_3mf(zf)
 
 
             # Find all plate gcode files to determine available plates
             # Find all plate gcode files to determine available plates
             gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
             gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
@@ -2423,6 +2429,8 @@ async def get_library_file_plates(
         "filename": lib_file.filename,
         "filename": lib_file.filename,
         "plates": plates,
         "plates": plates,
         "is_multi_plate": len(plates) > 1,
         "is_multi_plate": len(plates) > 1,
+        "embedded_printer": embedded_presets["printer"],
+        "embedded_process": embedded_presets["process"],
     }
     }
 
 
 
 

+ 39 - 0
backend/app/utils/threemf_tools.py

@@ -267,6 +267,45 @@ def extract_filament_properties_from_3mf(file_path: Path) -> dict[int, dict]:
     return properties
     return properties
 
 
 
 
+def _first_settings_id(value: object) -> str | None:
+    """A ``*_settings_id`` value is usually a string, occasionally a list (one
+    entry per extruder). Return the first non-empty string, else None."""
+    if isinstance(value, str):
+        return value.strip() or None
+    if isinstance(value, list):
+        for item in value:
+            if isinstance(item, str) and item.strip():
+                return item.strip()
+    return None
+
+
+def extract_embedded_presets_from_3mf(zf: zipfile.ZipFile) -> dict[str, str | None]:
+    """Read the printer / process preset names a 3MF project was prepared with.
+
+    BambuStudio / OrcaSlicer write the chosen preset names into
+    ``Metadata/project_settings.config`` (``printer_settings_id`` and
+    ``print_settings_id``). The SliceModal uses them to default its printer
+    and process dropdowns to what the file was sliced for (#1325) instead of
+    blindly taking the first listed preset.
+
+    Returns ``{"printer": <name|None>, "process": <name|None>}``. Every failure
+    mode (missing config, malformed JSON, unexpected shape) yields ``None``
+    values so the modal falls back to its own defaults.
+    """
+    result: dict[str, str | None] = {"printer": None, "process": None}
+    try:
+        if "Metadata/project_settings.config" not in zf.namelist():
+            return result
+        data = json.loads(zf.read("Metadata/project_settings.config").decode())
+    except (KeyError, ValueError, OSError):
+        return result
+    if not isinstance(data, dict):
+        return result
+    result["printer"] = _first_settings_id(data.get("printer_settings_id"))
+    result["process"] = _first_settings_id(data.get("print_settings_id"))
+    return result
+
+
 def extract_nozzle_mapping_from_3mf(zf: zipfile.ZipFile) -> dict[int, int] | None:
 def extract_nozzle_mapping_from_3mf(zf: zipfile.ZipFile) -> dict[int, int] | None:
     """Extract per-slot nozzle/extruder mapping from a 3MF file.
     """Extract per-slot nozzle/extruder mapping from a 3MF file.
 
 

+ 55 - 0
backend/tests/unit/test_threemf_tools.py

@@ -10,6 +10,7 @@ import math
 import zipfile
 import zipfile
 
 
 from backend.app.utils.threemf_tools import (
 from backend.app.utils.threemf_tools import (
+    extract_embedded_presets_from_3mf,
     extract_filament_usage_from_3mf,
     extract_filament_usage_from_3mf,
     extract_plate_extruder_set_from_3mf,
     extract_plate_extruder_set_from_3mf,
     extract_project_filaments_from_3mf,
     extract_project_filaments_from_3mf,
@@ -645,3 +646,57 @@ class TestExtractPlateExtruderSetFrom3mf:
             # Top-level metadata still works; missing component model file
             # Top-level metadata still works; missing component model file
             # is silently skipped without crashing.
             # is silently skipped without crashing.
             assert extract_plate_extruder_set_from_3mf(zf, plate_id=1) == {2}
             assert extract_plate_extruder_set_from_3mf(zf, plate_id=1) == {2}
+
+
+class TestExtractEmbeddedPresetsFrom3mf:
+    """Printer / process preset names read from project_settings.config so the
+    SliceModal can default its dropdowns to the file's own config (#1325)."""
+
+    def test_extracts_printer_and_process(self):
+        config = json.dumps(
+            {
+                "printer_settings_id": "Bambu Lab X1 Carbon 0.4 nozzle",
+                "print_settings_id": "0.20mm Standard @BBL X1C",
+                "filament_settings_id": ["Bambu PLA Basic @BBL X1C"],
+            }
+        )
+        with _make_3mf_with({"Metadata/project_settings.config": config}) as zf:
+            assert extract_embedded_presets_from_3mf(zf) == {
+                "printer": "Bambu Lab X1 Carbon 0.4 nozzle",
+                "process": "0.20mm Standard @BBL X1C",
+            }
+
+    def test_settings_id_as_list_takes_first(self):
+        # Some exports write *_settings_id as a per-extruder list.
+        config = json.dumps(
+            {
+                "printer_settings_id": ["Bambu Lab A1 0.4 nozzle"],
+                "print_settings_id": ["0.16mm Optimal @BBL A1", "0.20mm @BBL A1"],
+            }
+        )
+        with _make_3mf_with({"Metadata/project_settings.config": config}) as zf:
+            result = extract_embedded_presets_from_3mf(zf)
+            assert result["printer"] == "Bambu Lab A1 0.4 nozzle"
+            assert result["process"] == "0.16mm Optimal @BBL A1"
+
+    def test_missing_config_returns_none_values(self):
+        with _make_3mf_with({"3D/3dmodel.model": "<model/>"}) as zf:
+            assert extract_embedded_presets_from_3mf(zf) == {
+                "printer": None,
+                "process": None,
+            }
+
+    def test_malformed_json_returns_none_values(self):
+        with _make_3mf_with({"Metadata/project_settings.config": "not json"}) as zf:
+            assert extract_embedded_presets_from_3mf(zf) == {
+                "printer": None,
+                "process": None,
+            }
+
+    def test_blank_and_absent_keys_yield_none(self):
+        config = json.dumps({"printer_settings_id": "  ", "other": "x"})
+        with _make_3mf_with({"Metadata/project_settings.config": config}) as zf:
+            assert extract_embedded_presets_from_3mf(zf) == {
+                "printer": None,
+                "process": None,
+            }

+ 48 - 11
frontend/src/components/SliceModal.tsx

@@ -63,15 +63,40 @@ function findPreset(
   return by[ref.source][slot].find((p) => p.id === ref.id) ?? null;
   return by[ref.source][slot].find((p) => p.id === ref.id) ?? null;
 }
 }
 
 
-// Process default (#1325): first preset compatible with the selected printer
-// in tier order, then the first whose compatibility is merely unknown, then
-// plain priority. Keeps the pre-pick honest with the printer filter instead
-// of blindly taking list[0].
+// Find a preset by exact name across tiers (local → cloud → standard). Used
+// to honour the printer / process preset names a 3MF was prepared with.
+function findPresetByName(
+  by: UnifiedPresetsResponse,
+  slot: Slot,
+  name: string | null | undefined,
+): PresetRef | null {
+  if (!name) return null;
+  for (const tier of SLICE_MODAL_TIER_ORDER) {
+    const p = by[tier][slot].find((x) => x.name === name);
+    if (p) return { source: p.source, id: p.id };
+  }
+  return null;
+}
+
+// Process default: honour the process preset the 3MF was prepared with
+// (preferredName) when it's available and not incompatible with the selected
+// printer; otherwise the first preset compatible with the printer in tier
+// order, then the first whose compatibility is merely unknown, then plain
+// priority. Keeps the pre-pick honest with both the embedded config and the
+// printer filter instead of blindly taking list[0] (#1325).
 function pickProcessDefault(
 function pickProcessDefault(
   by: UnifiedPresetsResponse,
   by: UnifiedPresetsResponse,
   printerName: string | null,
   printerName: string | null,
   printerCode: string | null,
   printerCode: string | null,
+  preferredName?: string | null,
 ): PresetRef | null {
 ): PresetRef | null {
+  const preferred = findPresetByName(by, 'process', preferredName);
+  if (preferred) {
+    const p = findPreset(by, preferred, 'process');
+    if (p && presetCompatibility(p, printerName, printerCode) !== 'mismatch') {
+      return preferred;
+    }
+  }
   for (const wanted of ['match', 'unknown'] as const) {
   for (const wanted of ['match', 'unknown'] as const) {
     for (const tier of SLICE_MODAL_TIER_ORDER) {
     for (const tier of SLICE_MODAL_TIER_ORDER) {
       for (const p of by[tier].process) {
       for (const p of by[tier].process) {
@@ -409,13 +434,25 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
     [selectedPrinterName],
     [selectedPrinterName],
   );
   );
 
 
-  // Printer pre-pick: see SLICE_MODAL_TIER_ORDER. Runs once when presets
-  // first arrive; subsequent re-renders preserve any manual choice.
+  // Printer / process preset names the source 3MF was prepared with. The
+  // plates query resolves before the presets query (the latter is gated on
+  // it), so these are known by the time the pre-pick effects run.
+  const embeddedPrinter = platesQuery.data?.embedded_printer ?? null;
+  const embeddedProcess = platesQuery.data?.embedded_process ?? null;
+
+  // Printer pre-pick: defaults to the printer the 3MF was prepared for when
+  // that preset is available, else the first listed printer. Runs once when
+  // presets first arrive; later re-renders preserve any manual choice.
   useEffect(() => {
   useEffect(() => {
-    if (!presetsQuery.data) return;
-    if (printerPreset == null) setPrinterPreset(pickDefault(presetsQuery.data, 'printer'));
+    const data = presetsQuery.data;
+    if (!data) return;
+    if (printerPreset == null) {
+      setPrinterPreset(
+        findPresetByName(data, 'printer', embeddedPrinter) ?? pickDefault(data, 'printer'),
+      );
+    }
     // eslint-disable-next-line react-hooks/exhaustive-deps
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [presetsQuery.data]);
+  }, [presetsQuery.data, embeddedPrinter]);
 
 
   // Process pre-pick / re-pick (#1325): defaults to a process compatible with
   // Process pre-pick / re-pick (#1325): defaults to a process compatible with
   // the selected printer, and re-defaults when a printer change leaves the
   // the selected printer, and re-defaults when a printer change leaves the
@@ -430,9 +467,9 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
           return current;
           return current;
         }
         }
       }
       }
-      return pickProcessDefault(data, selectedPrinterName, selectedPrinterCode);
+      return pickProcessDefault(data, selectedPrinterName, selectedPrinterCode, embeddedProcess);
     });
     });
-  }, [presetsQuery.data, selectedPrinterName, selectedPrinterCode]);
+  }, [presetsQuery.data, selectedPrinterName, selectedPrinterCode, embeddedProcess]);
 
 
   // Filament pre-pick: re-runs when the active filament-slot count changes
   // Filament pre-pick: re-runs when the active filament-slot count changes
   // (plate selection, single-plate metadata arriving) or the selected printer
   // (plate selection, single-plate metadata arriving) or the selected printer

+ 11 - 2
frontend/src/types/plates.ts

@@ -27,7 +27,16 @@ export interface PlateMetadata {
   filaments: PlateFilament[];
   filaments: PlateFilament[];
 }
 }
 
 
-export interface ArchivePlatesResponse {
+// Printer / process preset names the source 3MF was prepared with, read from
+// its project_settings.config. Used by the SliceModal to default its printer
+// and process dropdowns (#1325). Null / absent when the file carries no
+// embedded slicer config (STL, plain model 3MF, parse failure).
+interface EmbeddedPresets {
+  embedded_printer?: string | null;
+  embedded_process?: string | null;
+}
+
+export interface ArchivePlatesResponse extends EmbeddedPresets {
   archive_id: number;
   archive_id: number;
   filename: string;
   filename: string;
   plates: PlateMetadata[];
   plates: PlateMetadata[];
@@ -35,7 +44,7 @@ export interface ArchivePlatesResponse {
   has_gcode?: boolean;
   has_gcode?: boolean;
 }
 }
 
 
-export interface LibraryFilePlatesResponse {
+export interface LibraryFilePlatesResponse extends EmbeddedPresets {
   file_id: number;
   file_id: number;
   filename: string;
   filename: string;
   plates: PlateMetadata[];
   plates: PlateMetadata[];

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/index-D23KVJz4.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-RSlhNz-h.js"></script>
+    <script type="module" crossorigin src="/assets/index-D23KVJz4.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-QwBJJHPy.css">
     <link rel="stylesheet" crossorigin href="/assets/index-QwBJJHPy.css">
   </head>
   </head>
   <body>
   <body>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini