Files
bambuddy/backend/tests/integration/test_design_settings_plates.py
T
maziggy 8551e32f14 feat(slicer): keep the designer's print settings when re-slicing for another printer (#2622)
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.
2026-07-28 14:49:50 +02:00

101 lines
3.9 KiB
Python

"""The plates endpoints must surface the designer's changed settings (#2622).
Parsing is covered in ``unit/test_design_settings.py``. What is asserted here is
the wiring: SliceModal reads ``design_overrides`` off the plates response, so a
correct parser that never reaches the payload is a feature that silently does
nothing.
"""
import json
import zipfile
from pathlib import Path
import pytest
from httpx import AsyncClient
def _designed_3mf(path: Path, *, with_deviations: bool = True) -> None:
"""A Bambu-style project 3MF, optionally carrying designer deviations."""
config = {
"print_settings_id": "0.20mm Standard @BBL A1",
"printer_settings_id": "Bambu Lab A1 0.4 nozzle",
"filament_settings_id": ["Bambu PLA Basic @BBL A1"],
"wall_loops": "5",
"outer_wall_speed": "200",
"machine_start_gcode": "G28 ; designer printer",
"different_settings_to_system": (
["wall_loops;outer_wall_speed", "", "machine_start_gcode"] if with_deviations else ["", "", ""]
),
}
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("Metadata/plate_1.gcode", "G0\n")
zf.writestr("Metadata/project_settings.config", json.dumps(config))
@pytest.fixture
def _patch_base_dir(monkeypatch, tmp_path):
from backend.app.core.config import settings
monkeypatch.setattr(settings, "base_dir", tmp_path)
return tmp_path
class TestArchivePlatesDesignOverrides:
@pytest.mark.asyncio
@pytest.mark.integration
async def test_returns_the_process_deviations_with_classification(
self, async_client: AsyncClient, archive_factory, printer_factory, _patch_base_dir
):
_designed_3mf(_patch_base_dir / "designed.3mf")
printer = await printer_factory()
archive = await archive_factory(printer.id, filename="designed.3mf", file_path="designed.3mf")
response = await async_client.get(f"/api/v1/archives/{archive.id}/plates")
assert response.status_code == 200
overrides = response.json()["design_overrides"]
assert [o["key"] for o in overrides] == ["outer_wall_speed", "wall_loops"]
by_key = {o["key"]: o for o in overrides}
assert by_key["wall_loops"] == {"key": "wall_loops", "value": "5", "printer_coupled": False}
assert by_key["outer_wall_speed"]["printer_coupled"] is True
# The printer slot must never leak into the process list.
assert "machine_start_gcode" not in by_key
@pytest.mark.asyncio
@pytest.mark.integration
async def test_empty_for_a_file_that_changes_nothing(
self, async_client: AsyncClient, archive_factory, printer_factory, _patch_base_dir
):
_designed_3mf(_patch_base_dir / "stock.3mf", with_deviations=False)
printer = await printer_factory()
archive = await archive_factory(printer.id, filename="stock.3mf", file_path="stock.3mf")
response = await async_client.get(f"/api/v1/archives/{archive.id}/plates")
assert response.status_code == 200
assert response.json()["design_overrides"] == []
class TestLibraryPlatesDesignOverrides:
@pytest.mark.asyncio
@pytest.mark.integration
async def test_returns_the_process_deviations(self, async_client: AsyncClient, db_session, tmp_path):
from backend.app.models.library import LibraryFile
path = tmp_path / "designed.3mf"
_designed_3mf(path)
lib_file = LibraryFile(
filename="designed.3mf",
file_path=str(path),
file_type="3mf",
file_size=path.stat().st_size,
)
db_session.add(lib_file)
await db_session.commit()
await db_session.refresh(lib_file)
response = await async_client.get(f"/api/v1/library/files/{lib_file.id}/plates")
assert response.status_code == 200
assert [o["key"] for o in response.json()["design_overrides"]] == ["outer_wall_speed", "wall_loops"]