Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion backend/app/api/routes/slicer_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ async def _fetch_orca_cloud_presets(
filament_colour=filament_colour,
filament_vendor=filament_vendor,
)
if slot == "printer":
# Orca Cloud hands us the whole profile content, so a printer
# profile that states its bed states it right here. A cloud
# profile is a user's own export and is normally already
# flattened; one that only `inherits:` a bundled base states
# nothing and stays `None` — Bambuddy has no copy of the
# slicer's profile tree to walk, which is exactly why the
# sidecar resolves the standard tier.
preset.printable_area = _parse_printable_area(content.get("printable_area"))
if slot in ("process", "filament"):
# The profile's own compatible-printer list, straight out of
# the content Orca already hands us (#2628). Without it the
Expand Down Expand Up @@ -314,6 +323,14 @@ async def _fetch_local_presets(db: AsyncSession) -> dict[str, list[UnifiedPreset
if slot is None:
continue
preset = UnifiedPreset(id=str(p.id), name=p.name, source="local")
if slot == "printer":
# `LocalPreset.setting` is the full *resolved* profile blob the
# importer stored, so an imported printer preset carries its bed
# here even though no column exists for it. Re-parsing the blob is
# what `_parse_filament_metadata` already does for the filament
# slot; adding a column would need a migration for a value only
# this listing reads.
preset.printable_area = _parse_local_printable_area(p.setting)
if slot == "filament":
preset.filament_type, preset.filament_colour = _parse_filament_metadata(p.setting)
# The MODEL COLUMN, not the stored profile blob (#58). This
Expand Down Expand Up @@ -398,6 +415,56 @@ def _first_scalar(value: object) -> str | None:
return None


def _parse_local_printable_area(setting_json: str | None) -> list[str] | None:
"""``printable_area`` out of a stored local-preset blob.

Defensive parse, same contract as :func:`_parse_filament_metadata`: any
error returns ``None`` so a corrupt row costs one printer its bed rather
than costing the caller the whole local tier.
"""
if not setting_json:
return None
try:
data = json.loads(setting_json)
except (ValueError, TypeError):
return None
if not isinstance(data, dict):
return None
return _parse_printable_area(data.get("printable_area"))


def _parse_printable_area(value: object) -> list[str] | None:
"""Normalise a declared bed outline to a list of ``"<x>x<y>"`` corner points.

Container shape only — **never geometry**. Every declared point survives,
in order, because the outline is not always an origin-anchored rectangle:
8 profiles in OrcaSlicer's vendor tree declare 72-point round delta beds,
3 declare 6 points and 3 declare 239. Reducing here would flatten those
into a rectangle that lies about where a model may be placed, which is
precisely why the sidecar hands the polygon over raw.

What does get normalised is the packaging, which the profile trees are not
consistent about — one vendor writes the whole polygon as a single
comma-joined string, and one BBL preset ships a stray space inside a point.
The sidecar already normalises both, so in practice this only fires for the
local and orca_cloud tiers, which read profile JSON directly.

Anything that is not a polygon of at least 3 usable points returns
``None``. That keeps ``None`` meaning "no bed known" rather than degrading
into a bed whose bounding box happens to be zero-sized — the two must stay
distinguishable, because a consumer treats the first as "fall back to the
default plate" and the second as "place everything at the origin".
"""
if isinstance(value, str):
raw: list[object] = list(value.split(","))
elif isinstance(value, list):
raw = list(value)
else:
return None
points = [p.strip() for p in raw if isinstance(p, str) and p.strip()]
return points if len(points) >= 3 else None


async def _fetch_bundled_presets(db: AsyncSession, *, refresh: bool = False) -> dict[str, list[UnifiedPreset]]:
"""Standard slicer-bundled profiles via the sidecar's /profiles/bundled.

Expand Down Expand Up @@ -433,7 +500,23 @@ async def _fetch_bundled_presets(db: AsyncSession, *, refresh: bool = False) ->
continue
# Bundled presets are addressed by name (the slicer resolves them
# by name during the `inherits:` walk), so name doubles as id.
extra: dict[str, str | None] = {}
extra: dict[str, Any] = {}
if slot == "printer":
# The printer's bed outline, resolved by the sidecar through
# the bundled profile's `inherits:` chain — a BBL machine
# preset is a per-nozzle delta that declares no bed of its own
# (measured: the leaf states it for 4/44 presets in OrcaSlicer
# v2.3.2 and 7/56 in BambuStudio v02.07.01.57; through the walk
# it resolves for 44/44 and 56/56).
#
# **Only sidecar images built from orca-slicer-api#4 emit it.**
# Every deployment runs an older one until its image is
# rebuilt, so this reads `None` for the entire standard tier
# today and every consumer must cope with that — it is the
# normal state, not an error state. Read it anyway so a
# rebuilt sidecar starts supplying beds without a second
# change here (same pattern as `filament_vendor`, #58).
extra["printable_area"] = _parse_printable_area(entry.get("printable_area"))
if slot == "filament":
extra["filament_type"] = entry.get("filament_type")
extra["filament_colour"] = entry.get("filament_colour")
Expand Down Expand Up @@ -527,6 +610,16 @@ def _enrich_cloud_metadata(
not merely score badly, it would sit in a different heading from the
identically-named local copy of the same spool.

Bed merge (#68): ``printable_area`` rides the same name bridge on the
PRINTER slot. Only the standard tier resolves a real bed (the sidecar
walks the bundled profile's ``inherits:`` chain); Bambu Cloud carries no
profile content at all, and an Orca Cloud or local copy of the same
printer is often a delta that states no geometry. Without the bridge, the
bed a printer reports would depend on which tier happened to win dedup —
invisible to the user and not something they chose. There is no
name-parse fallback for this one: a bed cannot be guessed from a string,
and a hardcoded model→bed map was deliberately removed (CHANGELOG:1173).

Finally, every filament entry left without a vendor after the bridge gets
one parsed out of its NAME. This runs across all four tiers, not just the
standard one: the local tier's vendor column is nullable and rows imported
Expand Down Expand Up @@ -567,6 +660,34 @@ def _enrich_cloud_metadata(
if not p.filament_vendor:
p.filament_vendor = _parse_vendor_from_name(p.name)

# Bed bridge (#68). Same name bridge, printer slot: whichever copy of a
# printer preset knows its bed teaches the ones that don't.
#
# This is what keeps the feature working while deployments are mid-upgrade.
# A user picks "Bambu Lab H2D 0.4 nozzle" out of whichever tier ranks
# highest for them — Bambu Cloud never carries profile content at all, and
# an Orca Cloud or local copy may be a delta that only `inherits:` a
# bundled base. The standard tier is the one that resolves a real bed
# (through the sidecar's inheritance walk), and the identically-named entry
# in it is the same physical printer. Without the bridge the bed would
# depend on which tier happened to win dedup, which is not something the
# user chose or can see.
#
# Only ever fills a gap, and only with a real value: an entry that states
# its own bed keeps it, and `None` is never propagated over anything.
beds_by_name: dict[str, list[str]] = {}
for tier in (local, orca_cloud, standard):
for p in tier["printer"]:
if p.printable_area and p.name not in beds_by_name:
beds_by_name[p.name] = p.printable_area
if beds_by_name:
for tier in (local, orca_cloud, cloud, standard):
for p in tier["printer"]:
if p.printable_area is None:
borrowed = beds_by_name.get(p.name)
if borrowed:
p.printable_area = borrowed

# Compatibility bridge (#2628). Runs over both slots that carry the
# list, and in both directions between the cloud tiers — whichever copy
# of a profile knows its printers teaches the ones that don't.
Expand Down
35 changes: 35 additions & 0 deletions backend/app/schemas/slicer_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,40 @@ class UnifiedPreset(BaseModel):
vendor could be resolved at all; the frontend buckets those into a
trailing "Other" group rather than scattering them.

``printable_area`` is populated for the **printer** slot only: the bed
outline the slicer's own profile tree declares for that printer, as a list
of ``"<x>x<y>"`` corner points in bed millimetres — e.g.
``["0x0", "350x0", "350x320", "0x320"]`` for an H2D. It is **not** a
width/height pair, and it is deliberately carried in the slicer's own raw
shape: 8 profiles in OrcaSlicer's vendor tree declare 72-point round delta
beds, and reducing here would flatten a round or origin-offset bed into a
rectangle that silently lies about where a model may be placed. Consumers
reduce it themselves, to whatever they need (a bounding box for the
viewport, the polygon for a render outline).

This is the only per-printer geometry Bambuddy has: the ``Printer`` DB
model has no dimensions, ``PRINTER_MODEL_MAP`` has none, and
``printerPresetAxes.ts`` states outright that the preset NAME is the only
carrier of model and nozzle. A hardcoded model→bed map is not an option —
one existed and was deliberately removed in favour of reading
``printable_area`` (CHANGELOG:1173).

Resolution per tier, best source first:

- standard → the sidecar's ``printable_area``, resolved through the
bundled profile's ``inherits:`` chain (orca-slicer-api#4). **Only the
rebuilt fork images emit it.**
- orca_cloud → the profile content's own ``printable_area``
- local → the same key out of the stored resolved profile blob
- cloud → borrowed from a same-named entry in another tier; Bambu
Cloud's list response carries no profile content at all

``None`` means no bed could be resolved, and every deployment reads
``None`` for the whole standard tier until its sidecar image is rebuilt —
that is the state each one is in today, and the frontend must keep working
in it. **``None`` is never "a bed of size zero"**: a zero-area bed would
place a model at the origin, a missing one has to fall back to a default.

``compatible_printers`` is the slicer's own list of printer-preset names a
process / filament preset declares itself valid for. Populated for the
local tier (stored at import time); left ``None`` for cloud (no per-preset
Expand All @@ -68,6 +102,7 @@ class UnifiedPreset(BaseModel):
filament_colour: str | None = None
filament_vendor: str | None = None
compatible_printers: list[str] | None = None
printable_area: list[str] | None = None


class UnifiedPresetsBySlot(BaseModel):
Expand Down
24 changes: 24 additions & 0 deletions backend/app/services/slicer_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,30 @@ async def list_bundled_profiles(self) -> dict:
tier on both slicers — colour is a runtime spool attribute, not a
profile one — so an empty colour here is data, not a degraded response.

Printer entries additionally carry ``printable_area`` (#68): the bed
outline as a list of ``"<x>x<y>"`` corner points in bed millimetres,
e.g. ``["0x0", "350x0", "350x320", "0x320"]``. **Not** a width/height
pair — the raw polygon is passed through so a round or origin-offset
bed is not silently flattened into a rectangle, and Bambuddy reduces
it itself. Resolved through the same ``inherits:`` walk as the filament
fields, and for the same reason: a BBL machine preset is a per-nozzle
delta that declares no bed of its own, so a walk-less sidecar reports
``null`` for essentially the whole tier (measured on the bundled trees:
the leaf states it for 4/44 presets in OrcaSlicer v2.3.2 and 7/56 in
BambuStudio v02.07.01.57; through the walk, 44/44 and 56/56).

**Only sidecar images built from orca-slicer-api#4 emit it at all**, so
every deployment reads ``null`` here until its image is rebuilt. That
is the normal state, not an error state, and callers must keep working
in it — ``None`` means "no bed known, fall back to a default plate",
which is a different instruction from a bed of size zero.

The listing also contains ``type: "machine_model"`` catalogue entries
("Bambu Lab H2D" with no nozzle suffix) that describe a printer family
rather than a slicing preset. Those declare no bed anywhere in their
chain and report ``null`` — 11 of 55 printer entries on OrcaSlicer,
14 of 70 on BambuStudio.

Returns an empty-shaped dict when the sidecar is unreachable so the
unified-presets endpoint can degrade to "no standard tier" without
crashing the modal — cloud + local-imported profiles still render.
Expand Down
Loading