Explorar o código

Slicer: stock Bambu Cloud presets mark process changes too (#3497)

- signed in to Bambu Cloud, the stock process presets are picked from
  the cloud and the bundled twin is hidden (#3386), so the recording
  added for standard presets never ran for those users
- a cloud preset from the listing's "public" list now counts as the
  system preset too; the listing comes from the same cache as the
  preset listing, and one that can't be fetched counts as not stock
- private Bambu Cloud, Orca Cloud and local presets stay unmarked, and
  slices with them don't ask Bambu Cloud
maziggy hai 12 horas
pai
achega
f3f58c57c4

+ 10 - 4
backend/app/api/routes/library.py

@@ -4521,12 +4521,18 @@ async def _run_slicer_with_fallback(
 
     # #3497: the slicer CLI leaves different_settings_to_system empty, so the
     # keys set above are not marked as changed in the output 3MF and Bambu
-    # Studio reopens it with the stock preset's values. Only for a bundled
-    # (standard) process preset: the system preset is then the base those keys
-    # differ from. A cloud or local preset already is the user's own preset.
+    # Studio reopens it with the stock preset's values. Only for a system
+    # process preset, bundled (standard) or Bambu's stock copy on Bambu Cloud,
+    # which signed-in users pick instead (#3386): the system preset is then
+    # the base those keys differ from. A private cloud, Orca Cloud or local
+    # preset already is the user's own preset.
+    from backend.app.api.routes.slicer_presets import is_stock_cloud_preset
+
+    process_ref = request.process_preset
     process_changes = (
         changed_process_keys(process_as_picked, presets["process"])
-        if request.process_preset is not None and request.process_preset.source == "standard"
+        if process_ref is not None
+        and (process_ref.source == "standard" or await is_stock_cloud_preset(db, user, process_ref, "process"))
         else []
     )
 

+ 12 - 0
backend/app/api/routes/slicer_presets.py

@@ -817,6 +817,18 @@ async def load_stock_aliases(db: AsyncSession, user: User | None) -> dict[str, P
     return _stock_aliases(cloud, standard)
 
 
+async def is_stock_cloud_preset(db: AsyncSession, user: User | None, ref: PresetRef, slot: str) -> bool:
+    """Whether ``ref`` is one of Bambu's stock presets on Bambu Cloud (#3497).
+
+    The cloud listing's "public" list, from the same cache as the listing; a
+    listing that can't be fetched counts as not stock.
+    """
+    if ref.source != "cloud":
+        return False
+    cloud, _status = await _fetch_cloud_presets(db, user)
+    return any(p.stock and p.id == ref.id for p in cloud.get(slot, []))
+
+
 def listed_ref(aliases: dict[str, PresetRef], ref: PresetRef) -> PresetRef:
     """The ref the listing shows for ``ref``: its cloud twin if hidden, else itself."""
     return aliases.get(f"{ref.source}:{ref.id}", ref)

+ 57 - 5
backend/tests/unit/test_process_changes_recorded_3497.py

@@ -17,6 +17,7 @@ import pytest
 
 from backend.app.api.routes.library import _run_slicer_with_fallback
 from backend.app.schemas.slicer import PresetRef, SliceRequest
+from backend.app.schemas.slicer_presets import UnifiedPreset
 from backend.app.services.design_settings import changed_process_keys, record_process_changes
 
 pytestmark = pytest.mark.unit
@@ -118,10 +119,12 @@ class TestChangedKeys:
         assert changed_process_keys("{", json.dumps({"a": "1"})) == []
 
 
-def _request(process_source: str = "standard", **overrides) -> SliceRequest:
+def _request(
+    process_source: str = "standard", process_id: str = "0.20mm Standard @BBL A1M", **overrides
+) -> SliceRequest:
     return SliceRequest(
         printer_preset=PresetRef(source="standard", id="Bambu Lab A1 mini 0.4 nozzle"),
-        process_preset=PresetRef(source=process_source, id="0.20mm Standard @BBL A1M"),
+        process_preset=PresetRef(source=process_source, id=process_id),
         filament_presets=[PresetRef(source="standard", id="Bambu PLA Basic @BBL A1M")],
         export_3mf=True,
         **overrides,
@@ -151,9 +154,17 @@ def _source_3mf() -> bytes:
     return buffer.getvalue()
 
 
-async def _slice(request: SliceRequest, *, model: str = "stl", embedded_fallback: bool = False):
+def _cloud_listing(*presets: UnifiedPreset, status: str = "ok"):
+    slots = {"printer": [], "process": list(presets), "filament": []}
+    return AsyncMock(return_value=(slots, status))
+
+
+async def _slice(request: SliceRequest, *, model: str = "stl", embedded_fallback: bool = False, cloud_listing=None):
     from backend.app.services import preset_resolver, slicer_api as slicer_api_module
 
+    if cloud_listing is None:
+        cloud_listing = _cloud_listing(status="not_authenticated")
+
     result = slicer_api_module.SliceResult(
         content=_sliced_3mf(["", "", ""]),
         print_time_seconds=600,
@@ -183,6 +194,7 @@ async def _slice(request: SliceRequest, *, model: str = "stl", embedded_fallback
         patch("backend.app.services.preset_resolver.resolve_preset_ref", new=AsyncMock(side_effect=_resolve)),
         patch.object(slicer_api_module, "SlicerApiService", return_value=service),
         patch.object(slicer_api_module, "get_stall_timeout_seconds", new=AsyncMock(return_value=60.0)),
+        patch("backend.app.api.routes.slicer_presets._fetch_cloud_presets", new=cloud_listing),
     ):
         result, _ = await _run_slicer_with_fallback(
             SimpleNamespace(get=AsyncMock(return_value=None)),
@@ -223,8 +235,48 @@ class TestTheSlice:
     async def test_no_overrides_lists_nothing(self):
         assert await _slice(_request()) == ["", "", ""]
 
-    async def test_a_cloud_or_local_process_preset_lists_nothing(self):
-        listed = await _slice(_request(process_source="local", process_overrides={"sparse_infill_density": "40%"}))
+    async def test_a_local_process_preset_lists_nothing(self):
+        cloud_listing = _cloud_listing(status="not_authenticated")
+        listed = await _slice(
+            _request(process_source="local", process_overrides={"sparse_infill_density": "40%"}),
+            cloud_listing=cloud_listing,
+        )
+
+        assert listed == ["", "", ""]
+        # Nor does it ask Bambu Cloud.
+        cloud_listing.assert_not_awaited()
+
+    async def test_bambus_stock_process_preset_on_bambu_cloud_lists_them(self):
+        # Signed in, the stock presets are picked from Bambu Cloud (#3386).
+        listed = await _slice(
+            _request(
+                process_source="cloud",
+                process_id="GP000",
+                process_overrides={"sparse_infill_density": "40%", "sparse_infill_pattern": "gyroid"},
+            ),
+            cloud_listing=_cloud_listing(
+                UnifiedPreset(id="GP000", name="0.20mm Standard @BBL A1M", source="cloud", stock=True)
+            ),
+        )
+
+        assert listed == ["sparse_infill_density;sparse_infill_pattern", "", ""]
+
+    async def test_a_private_bambu_cloud_process_preset_lists_nothing(self):
+        listed = await _slice(
+            _request(process_source="cloud", process_id="PPUS1", process_overrides={"sparse_infill_density": "40%"}),
+            cloud_listing=_cloud_listing(
+                UnifiedPreset(id="PPUS1", name="My 0.20mm", source="cloud", stock=False),
+                UnifiedPreset(id="GP000", name="0.20mm Standard @BBL A1M", source="cloud", stock=True),
+            ),
+        )
+
+        assert listed == ["", "", ""]
+
+    async def test_a_cloud_preset_without_the_listing_lists_nothing(self):
+        listed = await _slice(
+            _request(process_source="cloud", process_id="GP000", process_overrides={"sparse_infill_density": "40%"}),
+            cloud_listing=_cloud_listing(status="unreachable"),
+        )
 
         assert listed == ["", "", ""]