slicer_presets.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. """Pydantic schemas for the unified slicer-presets endpoint.
  2. The SliceModal pulls printer/process/filament options from three sources, in
  3. priority order: cloud (the user's Bambu Cloud account), local (DB-backed
  4. imported profiles), and standard (slicer-bundled stock profiles). The endpoint
  5. returns all three lists with name-based dedup applied so each preset appears
  6. exactly once across the response.
  7. """
  8. from typing import Literal
  9. from pydantic import BaseModel, Field
  10. from backend.app.schemas.slicer import PresetRef
  11. CloudStatus = Literal["ok", "not_authenticated", "expired", "unreachable"]
  12. class UnifiedPreset(BaseModel):
  13. """A single printer/process/filament preset with its source.
  14. The ``id`` shape varies by source:
  15. - cloud → Bambu Cloud setting_id (e.g. ``"PFUS9ac902733670a9"``)
  16. - local → stringified DB row id from ``local_presets``
  17. - standard → preset name as written in the bundled JSON (the slicer
  18. resolves bundled profiles by name during inheritance walk)
  19. The frontend treats ``id`` as opaque; the slice dispatch path uses
  20. ``(source, id)`` to fetch / pass the preset content to the sidecar.
  21. ``filament_type`` and ``filament_colour`` are populated for the filament
  22. slot only — they let the SliceModal pre-pick a preset per plate slot in
  23. the multi-color flow by matching against the source 3MF's per-slot type
  24. and color. Populated when the underlying preset JSON exposes them; left
  25. as ``None`` on bundled profiles where colour is a runtime spool attribute.
  26. ``compatible_printers`` is the slicer's own list of printer-preset names a
  27. process / filament preset declares itself valid for. Populated for the
  28. local tier (stored at import time); left ``None`` for cloud (no per-preset
  29. detail is fetched — rate limits) and standard (the sidecar's bundled
  30. listing doesn't expose it). The SliceModal uses it to filter the
  31. process / filament dropdowns by the selected printer (#1325); when it is
  32. ``None`` the modal falls back to matching the preset name against the
  33. ``@BBL <code>`` printer-model registry.
  34. ``inherits`` is the preset a printer preset was saved from, for the local
  35. and OrcaSlicer Cloud tiers. A printer preset saved under a name of its own
  36. ("Bambu Lab H2D 0.4 nozzle - Skew") is listed in no process's
  37. ``compatible_printers`` and its name says nothing reliable about the
  38. model, so the SliceModal matches against this parent as well (#3250).
  39. OrcaSlicer Cloud filament presets carry it too: one saved with only the
  40. keys the user changed takes its material from this parent (#3403).
  41. ``stock`` marks a Bambu Cloud preset as one of Bambu's own (the cloud's
  42. "public" list) rather than the user's. Internal only: the listing drops
  43. the Standard presets of the same names (#3386).
  44. """
  45. id: str
  46. name: str
  47. source: Literal["orca_cloud", "cloud", "local", "standard"]
  48. filament_type: str | None = None
  49. filament_colour: str | None = None
  50. compatible_printers: list[str] | None = None
  51. inherits: str | None = None
  52. stock: bool = Field(default=False, exclude=True)
  53. class UnifiedPresetsBySlot(BaseModel):
  54. """Three slots in the order Bambu Studio / OrcaSlicer use."""
  55. printer: list[UnifiedPreset] = []
  56. process: list[UnifiedPreset] = []
  57. filament: list[UnifiedPreset] = []
  58. class UnifiedPresetsResponse(BaseModel):
  59. """Every tier carries its full preset list — no cross-tier dedup, except
  60. that Bambu Cloud's stock presets are listed once, under Bambu Cloud (#3386).
  61. Priority order: ``local > orca_cloud > cloud > standard``. The order
  62. drives auto-pick (first non-empty tier wins, name-lookup walks tiers
  63. in this order, filament scoring tiebreaks by per-tier bonus) and
  64. determines the visual rendering order of the SliceModal's optgroups,
  65. but a name that exists in multiple tiers appears in EACH of their
  66. groups so the user can pick any source.
  67. ``cloud_status`` / ``orca_cloud_status`` let the frontend show a banner
  68. explaining why a cloud tier is empty when the user expected to see it
  69. (signed out / token expired / network down). Each tier has its own
  70. status because they can fail independently.
  71. ``stock_aliases`` maps each dropped Standard preset (``"standard:<id>"``)
  72. to its Bambu Cloud stock twin, so a pipeline or AMS slot saved with the
  73. Standard copy still finds it.
  74. """
  75. orca_cloud: UnifiedPresetsBySlot = UnifiedPresetsBySlot()
  76. cloud: UnifiedPresetsBySlot = UnifiedPresetsBySlot()
  77. local: UnifiedPresetsBySlot = UnifiedPresetsBySlot()
  78. standard: UnifiedPresetsBySlot = UnifiedPresetsBySlot()
  79. cloud_status: CloudStatus = "ok"
  80. orca_cloud_status: CloudStatus = "ok"
  81. stock_aliases: dict[str, PresetRef] = {}
  82. class LoadedSpoolPreset(BaseModel):
  83. """The filament profile Bambuddy recorded for a slot (``slot_preset_mappings``).
  84. ``tray_info_idx`` is the filament id the slot was configured with alongside
  85. it. The SliceModal compares it with the tray's live id the same way the
  86. slot card does, so a row that outlived a spool swap isn't trusted (#3216).
  87. """
  88. preset_id: str
  89. preset_name: str
  90. preset_source: str
  91. tray_info_idx: str | None = None
  92. class LoadedSpoolTray(BaseModel):
  93. """One AMS slot or external holder, as the printer reports it right now."""
  94. ams_id: int
  95. tray_id: int
  96. tray_type: str | None = None
  97. tray_sub_brands: str | None = None
  98. tray_color: str | None = None
  99. tray_info_idx: str | None = None
  100. # Firmware's presence bit. A non-RFID spool it can't identify is present
  101. # (True) but has no tray_type, which reads as "loaded, unconfigured".
  102. exists: bool | None = None
  103. state: int | None = None
  104. saved_preset: LoadedSpoolPreset | None = None
  105. class LoadedSpoolUnit(BaseModel):
  106. id: int
  107. is_ams_ht: bool
  108. trays: list[LoadedSpoolTray]
  109. class LoadedSpoolPrinter(BaseModel):
  110. id: int
  111. name: str
  112. # Short model name ("X1C", "H2D") — the form the slicer's @BBL tags use.
  113. model: str | None = None
  114. ams: list[LoadedSpoolUnit] = []
  115. # Only holders that currently have a spool in them.
  116. external: list[LoadedSpoolTray] = []
  117. # How many holders the printer has, loaded or not: two on a dual-nozzle
  118. # printer, whose holders are labelled left and right.
  119. external_holders: int = 0
  120. class LoadedSpoolsResponse(BaseModel):
  121. """Connected printers and what is loaded in them, for the SliceModal (#3172)."""
  122. printers: list[LoadedSpoolPrinter] = []