slicer.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. """Pydantic schemas for slice requests."""
  2. from typing import Literal
  3. from pydantic import BaseModel, Field, model_validator
  4. class PresetRef(BaseModel):
  5. """A source-aware reference to a printer / process / filament preset.
  6. The SliceModal pulls dropdown options from four tiers (orca_cloud /
  7. cloud / local / standard). At submit time the client sends one of these
  8. per slot so the backend knows where to fetch the preset content from at
  9. slice time. ``cloud`` is Bambu Cloud (kept as the bare name for backward
  10. compatibility with existing requests); ``orca_cloud`` is Orca Cloud.
  11. """
  12. source: Literal["orca_cloud", "cloud", "local", "standard"]
  13. id: str = Field(
  14. ...,
  15. description=(
  16. "Orca Cloud profile id, Bambu Cloud setting_id, local DB row id (stringified), or standard preset name."
  17. ),
  18. )
  19. class SliceRequest(BaseModel):
  20. """Body for `POST /library/files/{file_id}/slice`.
  21. Two preset shapes are accepted per slot for backwards-compatibility:
  22. - **Legacy** — bare integer ``*_preset_id`` fields point into the
  23. ``local_presets`` table. Existing clients (and stale browser tabs after
  24. a Bambuddy upgrade) keep working unchanged.
  25. - **Source-aware** — ``*_preset`` carries an explicit
  26. ``{source, id}``. Required for cloud / standard tiers; also accepted
  27. (and equivalent) for local presets when the client is on the new modal.
  28. Exactly one of each pair must be set; the validator normalises legacy
  29. integer ids into a ``PresetRef(source='local', id=str(id))`` so the
  30. downstream resolver only deals with one shape.
  31. """
  32. # Legacy fields — kept optional so older clients continue to work.
  33. printer_preset_id: int | None = Field(
  34. default=None,
  35. description="DEPRECATED: prefer printer_preset. LocalPreset id with preset_type='printer'.",
  36. )
  37. process_preset_id: int | None = Field(
  38. default=None,
  39. description="DEPRECATED: prefer process_preset. LocalPreset id with preset_type='process'.",
  40. )
  41. filament_preset_id: int | None = Field(
  42. default=None,
  43. description="DEPRECATED: prefer filament_preset. LocalPreset id with preset_type='filament'.",
  44. )
  45. # Source-aware fields — set by the new SliceModal.
  46. printer_preset: PresetRef | None = None
  47. process_preset: PresetRef | None = None
  48. filament_preset: PresetRef | None = None
  49. # Multi-color: one PresetRef per AMS slot the source plate uses. Order is
  50. # significant — the slicer matches index-by-index against the plate's
  51. # filament slots. Always preferred over the legacy singular field; the
  52. # validator promotes a singular field into ``[singular]`` when the list
  53. # is empty so older clients keep working.
  54. filament_presets: list[PresetRef] = Field(default_factory=list)
  55. plate: int | None = Field(
  56. default=None,
  57. ge=0,
  58. description=(
  59. "Plate number to slice. ``None`` defaults to plate 1 on the sidecar "
  60. "(matches the pre-multi-plate behaviour). ``0`` is the sidecar's "
  61. "'all plates' sentinel — produces a single multi-plate 3MF whose "
  62. "``Metadata/plate_N.gcode`` entries cover every plate in the "
  63. "source. ``>= 1`` slices that one plate."
  64. ),
  65. )
  66. export_3mf: bool = Field(
  67. default=False,
  68. description="If true, request a 3MF response with embedded G-code instead of raw G-code.",
  69. )
  70. bed_type: str | None = Field(
  71. default=None,
  72. max_length=64,
  73. description=(
  74. "Override the process preset's curr_bed_type for this slice. Canonical "
  75. "BambuStudio / OrcaSlicer values: 'Cool Plate', 'Engineering Plate', "
  76. "'High Temp Plate', 'Textured PEI Plate', 'Smooth PEI Plate', "
  77. "'Cool Plate (SuperTack)', 'Supertack Plate'. None ⇒ inherit from the "
  78. "process preset unchanged (#1337)."
  79. ),
  80. )
  81. @model_validator(mode="after")
  82. def normalise_preset_refs(self) -> "SliceRequest":
  83. """Each slot must end up with a `PresetRef` set. Legacy integer ids
  84. become `(source='local', id=str(int))` so the route handler only
  85. deals with the canonical shape. For filament: a non-empty
  86. ``filament_presets`` list satisfies the requirement on its own; an
  87. empty list falls back to the singular fields, which then promote
  88. into a one-element list.
  89. """
  90. for slot, ref_attr, legacy_attr in (
  91. ("printer", "printer_preset", "printer_preset_id"),
  92. ("process", "process_preset", "process_preset_id"),
  93. ):
  94. ref = getattr(self, ref_attr)
  95. legacy_id = getattr(self, legacy_attr)
  96. if ref is None and legacy_id is None:
  97. raise ValueError(
  98. f"{slot} preset is required: provide '{ref_attr}' (preferred) or legacy '{legacy_attr}'"
  99. )
  100. if ref is None:
  101. setattr(self, ref_attr, PresetRef(source="local", id=str(legacy_id)))
  102. # Filament accepts THREE shapes, in priority order:
  103. # 1. filament_presets — multi-color array (new clients)
  104. # 2. filament_preset — source-aware singular (single-color new clients)
  105. # 3. filament_preset_id — legacy bare integer (old clients)
  106. # The first non-empty shape wins; missing all three raises.
  107. if not self.filament_presets:
  108. if self.filament_preset is not None:
  109. self.filament_presets = [self.filament_preset]
  110. elif self.filament_preset_id is not None:
  111. fallback = PresetRef(source="local", id=str(self.filament_preset_id))
  112. self.filament_preset = fallback
  113. self.filament_presets = [fallback]
  114. else:
  115. raise ValueError(
  116. "filament preset is required: provide 'filament_presets' (preferred), "
  117. "'filament_preset', or legacy 'filament_preset_id'"
  118. )
  119. elif self.filament_preset is None:
  120. # Multi-color caller: backfill the singular from the first slot
  121. # so callers that still read the legacy field see a stable value.
  122. self.filament_preset = self.filament_presets[0]
  123. return self
  124. class SliceResponse(BaseModel):
  125. """Response from `POST /library/files/{file_id}/slice`. The result lands
  126. in the user's library as a new ``LibraryFile`` (in the same folder as
  127. the source)."""
  128. library_file_id: int
  129. name: str
  130. print_time_seconds: int
  131. filament_used_g: float
  132. filament_used_mm: float
  133. used_embedded_settings: bool = False
  134. class SliceArchiveResponse(BaseModel):
  135. """Response from `POST /archives/{archive_id}/slice`. The result lands
  136. in the user's archives as a new ``PrintArchive`` row, inheriting
  137. printer / project metadata from the source archive."""
  138. archive_id: int
  139. name: str
  140. print_time_seconds: int
  141. filament_used_g: float
  142. filament_used_mm: float
  143. used_embedded_settings: bool = False