mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
Published models often deviate from the stock Bambu profile on purpose - five walls, 100% infill, a 0.1mm first layer. Re-slicing one for a different printer discarded all of it: the picked process preset overrides the file's embedded settings, and that override is precisely what makes cross-printer re-slicing work, so it cannot just be dropped. "Slice as designed" (#2611) does not help - it is all-or-nothing and only offered when the picked printer already matches the design's target. The deviation list does not have to be computed. Bambu Studio writes it into the 3MF as different_settings_to_system, laid out as [process, *filaments, printer] - verified against real files at 2, 3 and 4 filament slots. The parser refuses any file whose array length contradicts its own filament count rather than guessing an index, since reading the printer slot as the process slot would carry the designer's machine_start_gcode onto a foreign printer. The slice dialog now lists exactly which print settings the author changed and what each was set to, with a checkbox per setting. Design intent - wall count, infill, layer and first-layer height, supports, seam, brim, ironing - is ticked by default. Printer-specific values - speeds, accelerations, jerk, fans, temperatures, prime-tower geometry - are listed with a badge but start unticked: tuned for the author's machine, they can be merely wrong on the target or outside the range its profile accepts, which fails the slice outright. Only ticked keys are sent, and only keys the source actually flags as changed are applied. Values are written into the outgoing process JSON, the same mechanism the support carry-over has used since #1881: for a Standard preset pick that JSON is an inherits stub, so the patch is the child in the chain and wins over the flattened parent. Process slot only - filament picks are honoured as chosen. The wiki's "this is not a settings merge" note under Slice as designed described the gap this closes; rewritten to point at the new panel. Translated in all locales; wiki updated. Covered by backend and frontend tests.
193 lines
8.4 KiB
Python
193 lines
8.4 KiB
Python
"""Pydantic schemas for slice requests."""
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
class PresetRef(BaseModel):
|
|
"""A source-aware reference to a printer / process / filament preset.
|
|
|
|
The SliceModal pulls dropdown options from four tiers (orca_cloud /
|
|
cloud / local / standard). At submit time the client sends one of these
|
|
per slot so the backend knows where to fetch the preset content from at
|
|
slice time. ``cloud`` is Bambu Cloud (kept as the bare name for backward
|
|
compatibility with existing requests); ``orca_cloud`` is Orca Cloud.
|
|
"""
|
|
|
|
source: Literal["orca_cloud", "cloud", "local", "standard"]
|
|
id: str = Field(
|
|
...,
|
|
description=(
|
|
"Orca Cloud profile id, Bambu Cloud setting_id, local DB row id (stringified), or standard preset name."
|
|
),
|
|
)
|
|
|
|
|
|
class SliceRequest(BaseModel):
|
|
"""Body for `POST /library/files/{file_id}/slice`.
|
|
|
|
Two preset shapes are accepted per slot for backwards-compatibility:
|
|
|
|
- **Legacy** — bare integer ``*_preset_id`` fields point into the
|
|
``local_presets`` table. Existing clients (and stale browser tabs after
|
|
a Bambuddy upgrade) keep working unchanged.
|
|
- **Source-aware** — ``*_preset`` carries an explicit
|
|
``{source, id}``. Required for cloud / standard tiers; also accepted
|
|
(and equivalent) for local presets when the client is on the new modal.
|
|
|
|
Exactly one of each pair must be set; the validator normalises legacy
|
|
integer ids into a ``PresetRef(source='local', id=str(id))`` so the
|
|
downstream resolver only deals with one shape.
|
|
"""
|
|
|
|
# Legacy fields — kept optional so older clients continue to work.
|
|
printer_preset_id: int | None = Field(
|
|
default=None,
|
|
description="DEPRECATED: prefer printer_preset. LocalPreset id with preset_type='printer'.",
|
|
)
|
|
process_preset_id: int | None = Field(
|
|
default=None,
|
|
description="DEPRECATED: prefer process_preset. LocalPreset id with preset_type='process'.",
|
|
)
|
|
filament_preset_id: int | None = Field(
|
|
default=None,
|
|
description="DEPRECATED: prefer filament_preset. LocalPreset id with preset_type='filament'.",
|
|
)
|
|
|
|
# Source-aware fields — set by the new SliceModal.
|
|
printer_preset: PresetRef | None = None
|
|
process_preset: PresetRef | None = None
|
|
filament_preset: PresetRef | None = None
|
|
|
|
# Multi-color: one PresetRef per AMS slot the source plate uses. Order is
|
|
# significant — the slicer matches index-by-index against the plate's
|
|
# filament slots. Always preferred over the legacy singular field; the
|
|
# validator promotes a singular field into ``[singular]`` when the list
|
|
# is empty so older clients keep working.
|
|
filament_presets: list[PresetRef] = Field(default_factory=list)
|
|
|
|
plate: int | None = Field(
|
|
default=None,
|
|
ge=0,
|
|
description=(
|
|
"Plate number to slice. ``None`` defaults to plate 1 on the sidecar "
|
|
"(matches the pre-multi-plate behaviour). ``0`` is the sidecar's "
|
|
"'all plates' sentinel — produces a single multi-plate 3MF whose "
|
|
"``Metadata/plate_N.gcode`` entries cover every plate in the "
|
|
"source. ``>= 1`` slices that one plate."
|
|
),
|
|
)
|
|
export_3mf: bool = Field(
|
|
default=False,
|
|
description="If true, request a 3MF response with embedded G-code instead of raw G-code.",
|
|
)
|
|
design_overrides: list[str] | None = Field(
|
|
default=None,
|
|
description=(
|
|
"3MF only. Process setting keys from the source file's "
|
|
"``different_settings_to_system`` to carry onto the picked process "
|
|
"preset (#2622) — the designer's own wall count, infill, first-layer "
|
|
"height and so on, which ``--load-settings`` would otherwise discard. "
|
|
"Only keys the source actually lists as changed are applied; anything "
|
|
"else is ignored. ``None``/empty means a plain profile slice."
|
|
),
|
|
)
|
|
use_embedded_settings: bool = Field(
|
|
default=False,
|
|
description=(
|
|
"3MF only. Slice using the file's embedded "
|
|
"``Metadata/project_settings.config`` (the designer's own tweaks — wall "
|
|
"count, infill, etc.) instead of the picked printer/process/filament "
|
|
"triplet. This is the 'slice as designed' path: no ``--load-settings`` "
|
|
"override, so a MakerWorld author's settings survive. Ignored for STL / "
|
|
"plain-model 3MF (no embedded profile to honour). The preset refs are "
|
|
"still required by the validator but go unused on this path. Only makes "
|
|
"sense when the picked printer matches the design's target model — the "
|
|
"UI gates the toggle on that; there is no cross-printer re-targeting here "
|
|
"(that is exactly what the profile path is for)."
|
|
),
|
|
)
|
|
bed_type: str | None = Field(
|
|
default=None,
|
|
max_length=64,
|
|
description=(
|
|
"Override the process preset's curr_bed_type for this slice. Canonical "
|
|
"BambuStudio / OrcaSlicer values: 'Cool Plate', 'Engineering Plate', "
|
|
"'High Temp Plate', 'Textured PEI Plate', 'Smooth PEI Plate', "
|
|
"'Cool Plate (SuperTack)', 'Supertack Plate'. None ⇒ inherit from the "
|
|
"process preset unchanged (#1337)."
|
|
),
|
|
)
|
|
|
|
@model_validator(mode="after")
|
|
def normalise_preset_refs(self) -> "SliceRequest":
|
|
"""Each slot must end up with a `PresetRef` set. Legacy integer ids
|
|
become `(source='local', id=str(int))` so the route handler only
|
|
deals with the canonical shape. For filament: a non-empty
|
|
``filament_presets`` list satisfies the requirement on its own; an
|
|
empty list falls back to the singular fields, which then promote
|
|
into a one-element list.
|
|
"""
|
|
for slot, ref_attr, legacy_attr in (
|
|
("printer", "printer_preset", "printer_preset_id"),
|
|
("process", "process_preset", "process_preset_id"),
|
|
):
|
|
ref = getattr(self, ref_attr)
|
|
legacy_id = getattr(self, legacy_attr)
|
|
if ref is None and legacy_id is None:
|
|
raise ValueError(
|
|
f"{slot} preset is required: provide '{ref_attr}' (preferred) or legacy '{legacy_attr}'"
|
|
)
|
|
if ref is None:
|
|
setattr(self, ref_attr, PresetRef(source="local", id=str(legacy_id)))
|
|
|
|
# Filament accepts THREE shapes, in priority order:
|
|
# 1. filament_presets — multi-color array (new clients)
|
|
# 2. filament_preset — source-aware singular (single-color new clients)
|
|
# 3. filament_preset_id — legacy bare integer (old clients)
|
|
# The first non-empty shape wins; missing all three raises.
|
|
if not self.filament_presets:
|
|
if self.filament_preset is not None:
|
|
self.filament_presets = [self.filament_preset]
|
|
elif self.filament_preset_id is not None:
|
|
fallback = PresetRef(source="local", id=str(self.filament_preset_id))
|
|
self.filament_preset = fallback
|
|
self.filament_presets = [fallback]
|
|
else:
|
|
raise ValueError(
|
|
"filament preset is required: provide 'filament_presets' (preferred), "
|
|
"'filament_preset', or legacy 'filament_preset_id'"
|
|
)
|
|
elif self.filament_preset is None:
|
|
# Multi-color caller: backfill the singular from the first slot
|
|
# so callers that still read the legacy field see a stable value.
|
|
self.filament_preset = self.filament_presets[0]
|
|
return self
|
|
|
|
|
|
class SliceResponse(BaseModel):
|
|
"""Response from `POST /library/files/{file_id}/slice`. The result lands
|
|
in the user's library as a new ``LibraryFile`` (in the same folder as
|
|
the source)."""
|
|
|
|
library_file_id: int
|
|
name: str
|
|
print_time_seconds: int
|
|
filament_used_g: float
|
|
filament_used_mm: float
|
|
used_embedded_settings: bool = False
|
|
|
|
|
|
class SliceArchiveResponse(BaseModel):
|
|
"""Response from `POST /archives/{archive_id}/slice`. The result lands
|
|
in the user's archives as a new ``PrintArchive`` row, inheriting
|
|
printer / project metadata from the source archive."""
|
|
|
|
archive_id: int
|
|
name: str
|
|
print_time_seconds: int
|
|
filament_used_g: float
|
|
filament_used_mm: float
|
|
used_embedded_settings: bool = False
|