Просмотр исходного кода

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.
maziggy 1 месяц назад
Родитель
Сommit
8551e32f14

+ 2 - 0
CHANGELOG.md

@@ -14,6 +14,8 @@ All notable changes to Bambuddy will be documented in this file.
 
 - **The plate-clear gate is now visible over MQTT, and can raise a notification (#2525, reporter @daschaefer)** — When a print reaches a terminal state, Bambuddy holds the queue until someone confirms the build plate is clear. That gate was visible only in the Web UI: the printer's own MQTT push reports nothing beyond `RUNNING`/`PAUSE`/`FAILED`/`FINISH`/`IDLE`, so an external automation could not tell "finished" from "finished and still waiting for a human". The per-printer status topic now carries an **`awaiting_plate_clear`** field, and every transition is additionally published on a new **retained** topic `bambuddy/printers/{serial}/plate_clear` (`{"awaiting": true|false, …}`). Retained and published from the flag itself rather than from printer telemetry, so a subscriber learns the current state of every printer the moment it connects — and the state stays correct after Auto Off powers a printer down, which stops telemetry entirely and would otherwise leave the status topic frozen at `false`. Publishing is edge-triggered: the queue re-asserts the flag on every dispatch, and no subscriber should see a "plate cleared" for a plate that was never dirty. A matching **Plate Clear Required** notification event was added, off by default on every provider because it fires after every print at the same moment as the print-complete alert. Acknowledging still goes through the existing `POST /printers/{id}/clear-plate`. Translated in all locales; wiki updated. Covered by backend tests.
 
+- **Re-slicing a model designed for another printer can now keep the designer's print settings (#2622, reporter @kpp39)** — 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, because the picked process preset overrides the file's embedded settings — that override is exactly what makes cross-printer re-slicing work, so it could not simply be dropped. "Slice as designed" (#2611) was no help here: it is all-or-nothing and only offered when your printer already matches the design's target. The slice dialog now shows a **Keep the designer's settings** panel listing precisely which print settings the author changed away from the stock profile, and what each was set to, with a checkbox per setting. **Design-intent settings** — wall count, infill density and pattern, layer and first-layer height, supports, seam position, brim, ironing — are ticked by default. **Printer-specific ones** — every speed and acceleration, jerk, fan speeds, temperatures, prime-tower geometry — are listed with a badge but start unticked, because a value tuned for the author's machine can be merely wrong on yours or outside the range your printer's profile accepts, which fails the slice outright. Nothing is guessed: Bambu Studio records the deviating-settings list inside the 3MF itself, so the panel shows the author's own change list. Only ticked settings are sent, only the process slot is carried (your filament picks are untouched), and the panel is hidden for files that change nothing. Translated in all locales; wiki updated. Covered by backend and frontend tests.
+
 - **The size-S printer card now shows remaining time, ETA and layer progress (#2674, reporter @jakestatefarm1101-alt)** — Size S existed for exactly one job: watching a whole fleet on one screen. But it rendered only the printer name, a status pip and a progress bar — every other block on the card is gated behind the expanded view — so it could not answer the question that view is for, "which printer finishes first". Dropping to S to fit more printers meant losing the information you dropped down to compare. The compact card now carries one line of metrics under the progress bar while a print is running: **remaining time**, **ETA** in your configured 12/24-hour format, and **layer progress** — the same values the Medium card already shows, using the same formatters and the same ETA styling so the two read alike. Each value is omitted individually when the printer doesn't report it, and the row holds its height when nothing is printing so cards don't shift as prints start and finish. Card dimensions and grid density are otherwise unchanged. Frontend-only. Wiki updated. Covered by tests.
 
 ### Fixed

+ 17 - 0
backend/app/api/routes/archives.py

@@ -29,6 +29,7 @@ from backend.app.schemas.archive import ArchiveResponse, ArchiveSlim, ArchiveSta
 from backend.app.schemas.print_log import PrintLogResponse
 from backend.app.schemas.slicer import SliceRequest
 from backend.app.services.archive import ArchiveService
+from backend.app.services.design_settings import overrides_from_config
 from backend.app.utils.http import build_content_disposition
 from backend.app.utils.safe_path import safe_join_under
 from backend.app.utils.threemf_tools import (
@@ -41,6 +42,9 @@ logger = logging.getLogger(__name__)
 
 router = APIRouter(prefix="/archives", tags=["archives"])
 
+# Path of the embedded slicer config inside a BambuStudio/OrcaSlicer 3MF.
+_PROJECT_SETTINGS_PATH = "Metadata/project_settings.config"
+
 
 def _safe_filename(filename: str) -> str:
     """Extract basename from a client-supplied filename, preventing path traversal.
@@ -3461,11 +3465,23 @@ async def get_archive_plates(
     # Printer / process preset names the 3MF was prepared with — used by the
     # SliceModal to default its dropdowns (#1325).
     embedded_presets: dict[str, str | None] = {"printer": None, "process": None}
+    # Process settings the designer changed away from the stock preset (#2622),
+    # offered in the SliceModal for a cross-printer re-slice. Same payload the
+    # library plates endpoint returns — SliceModal reads one shape for both.
+    design_overrides: list[dict] = []
 
     try:
         with zipfile.ZipFile(file_path, "r") as zf:
             namelist = zf.namelist()
             embedded_presets = extract_embedded_presets_from_3mf(zf)
+            if _PROJECT_SETTINGS_PATH in namelist:
+                try:
+                    design_overrides = [
+                        o._asdict()
+                        for o in overrides_from_config(json.loads(zf.read(_PROJECT_SETTINGS_PATH).decode("utf-8")))
+                    ]
+                except (ValueError, OSError, KeyError):
+                    design_overrides = []
 
             # Find all plate gcode files to determine available plates
             gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
@@ -3727,6 +3743,7 @@ async def get_archive_plates(
         "has_gcode": has_gcode,
         "embedded_printer": embedded_presets["printer"],
         "embedded_process": embedded_presets["process"],
+        "design_overrides": design_overrides,
     }
 
 

+ 36 - 0
backend/app/api/routes/library.py

@@ -64,6 +64,11 @@ from backend.app.schemas.library import (
 )
 from backend.app.schemas.slicer import SliceRequest, SliceResponse
 from backend.app.services.archive import ThreeMFParser
+from backend.app.services.design_settings import (
+    apply_design_overrides,
+    extract_design_process_overrides,
+    overrides_from_config,
+)
 from backend.app.services.plate_thumbnail import inject_plate_thumbnails_if_missing
 from backend.app.services.stl_thumbnail import MIN_USABLE_STL_BYTES, generate_stl_thumbnail
 from backend.app.utils.filename import InvalidFilenameError, validate_print_filename
@@ -77,6 +82,9 @@ logger = logging.getLogger(__name__)
 
 router = APIRouter(prefix="/library", tags=["library"])
 
+# Path of the embedded slicer config inside a BambuStudio/OrcaSlicer 3MF.
+_PROJECT_SETTINGS_PATH = "Metadata/project_settings.config"
+
 
 def _ensure_library_file_visible(
     library_file: LibraryFile | None,
@@ -2774,11 +2782,23 @@ async def get_library_file_plates(
     # SliceModal to default its dropdowns (#1325). Initialised here so the
     # final return never raises NameError when the file isn't a valid zip.
     embedded_presets: dict[str, str | None] = {"printer": None, "process": None}
+    # Process settings the designer changed away from the stock preset (#2622).
+    # Offered in the SliceModal so a cross-printer re-slice can carry them
+    # instead of silently losing them to the picked process profile.
+    design_overrides: list[dict] = []
 
     try:
         with zipfile.ZipFile(file_path, "r") as zf:
             namelist = zf.namelist()
             embedded_presets = extract_embedded_presets_from_3mf(zf)
+            if _PROJECT_SETTINGS_PATH in namelist:
+                try:
+                    design_overrides = [
+                        o._asdict()
+                        for o in overrides_from_config(json.loads(zf.read(_PROJECT_SETTINGS_PATH).decode("utf-8")))
+                    ]
+                except (ValueError, OSError, KeyError):
+                    design_overrides = []
 
             # Find all plate gcode files to determine available plates
             gcode_files = [n for n in namelist if n.startswith("Metadata/plate_") and n.endswith(".gcode")]
@@ -3007,6 +3027,7 @@ async def get_library_file_plates(
         "is_multi_plate": len(plates) > 1,
         "embedded_printer": embedded_presets["printer"],
         "embedded_process": embedded_presets["process"],
+        "design_overrides": design_overrides,
     }
 
 
@@ -3657,6 +3678,21 @@ async def _run_slicer_with_fallback(
         # with a PVA slot loaded but never used.
         presets["process"] = _patch_process_support_settings(presets["process"], primary_bytes)
 
+        # #2622: carry the designer's own process tweaks onto the picked preset.
+        # BambuStudio records exactly which keys deviate from the system preset
+        # in `different_settings_to_system`, so a MakerWorld author's 5 walls /
+        # 100% infill / 0.1mm first layer survive a re-slice for another printer
+        # instead of being flattened by --load-settings. Opt-in per key: only the
+        # keys the caller names are applied, and only if the source really lists
+        # them as changed. Runs after the #1881 support patch so an explicit
+        # design pick wins over the blanket support carry-over.
+        if request.design_overrides:
+            presets["process"] = apply_design_overrides(
+                presets["process"],
+                extract_design_process_overrides(primary_bytes),
+                request.design_overrides,
+            )
+
     used_embedded_settings = False
     # "Slice as designed" (#2611): honour the file's embedded
     # project_settings.config instead of the picked profile triplet. Only

+ 11 - 0
backend/app/schemas/slicer.py

@@ -82,6 +82,17 @@ class SliceRequest(BaseModel):
         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=(

+ 193 - 0
backend/app/services/design_settings.py

@@ -0,0 +1,193 @@
+"""Carry a 3MF designer's own process tweaks across a re-slice (#2622).
+
+A MakerWorld model is often published with deliberate deviations from the stock
+Bambu process preset — 5 walls, 100% infill, a 0.1mm first layer. Re-slicing that
+file for a different printer used to drop every one of them: ``--load-settings``
+is authoritative, so the picked process preset wins over the 3MF's embedded
+``Metadata/project_settings.config``.
+
+We do not have to *compute* what the designer changed. BambuStudio already did,
+and wrote the answer into the file:
+
+    different_settings_to_system = [
+        "enable_support;inner_wall_speed;sparse_infill_density;...",   # [0]  process
+        "filament_change_length;filament_prime_volume",                # [1..N] filaments
+        "machine_start_gcode;bed_custom_model;...",                    # [-1] printer
+    ]
+
+The array is ``1 + len(filament_settings_id) + 1`` long — verified against real
+files at 2, 3 and 4 filament slots. Index 0 is exactly the set of process keys
+that differ from the system preset, which is the reporter's step 1 for free: no
+baseline resolution, no shipping BBL profiles into Bambuddy, and no new endpoint
+on the slicer sidecar (which exposes bundled presets by name only, with no way to
+flatten one).
+
+Delivery is the mechanism ``_patch_process_support_settings`` already proved in
+#1881: write the values into the process JSON that goes out as ``--load-settings``.
+For a "standard" preset pick that JSON is a ``{inherits: …}`` stub, so the keys we
+write are the *child* in the inherits chain and win over the flattened parent.
+
+Not every key is safe to carry, though. Real files put ``inner_wall_speed``,
+``outer_wall_speed`` and ``prime_tower_max_speed`` in that list — values tuned for
+the designer's machine that can be plain wrong, or out of range, on the target.
+Those are classified :data:`PRINTER_COUPLED` and offered unticked; the caller
+decides. Nothing is applied that the caller did not ask for by name.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import zipfile
+from io import BytesIO
+from typing import Any, NamedTuple
+
+logger = logging.getLogger(__name__)
+
+_PROJECT_SETTINGS = "Metadata/project_settings.config"
+
+
+class DesignOverride(NamedTuple):
+    """One process setting the designer changed away from the system preset."""
+
+    key: str
+    value: Any
+    printer_coupled: bool
+
+
+# Process keys whose sane value depends on the machine, not on the design intent.
+# The designer picked these for *their* printer's kinematics, chamber and hotend;
+# carrying them onto another model risks a slice that is merely slower/uglier —
+# or a hard range-validation reject from the CLI, which is how the very first
+# slicer spike died. Offered, but never pre-selected.
+#
+# Matching is by exact key OR by suffix/substring rule below, because Bambu's
+# process schema has dozens of per-feature speed keys and an exhaustive literal
+# list would rot on every slicer release.
+_PRINTER_COUPLED_EXACT: frozenset[str] = frozenset(
+    {
+        "default_acceleration",
+        "independent_support_layer_height",
+        "precise_z_height",
+        "travel_acceleration",
+        "enable_wrapping_detection",
+    }
+)
+
+# Substring rules for the families that are always machine-coupled. Kept
+# deliberately narrow: "speed", "acceleration"/"accel" and "jerk" are the
+# kinematic families, "fan"/"temperature" follow the hotend and chamber, and
+# "prime_tower" follows the target's toolchange hardware.
+_PRINTER_COUPLED_SUBSTRINGS: tuple[str, ...] = (
+    # Prime-tower geometry (and whether there is one at all) follows the target's
+    # extruder count and bed, not the design — a real file carries five of these.
+    "prime_tower",
+    "_speed",
+    "speed_",
+    "acceleration",
+    "_accel",
+    "jerk",
+    "fan_speed",
+    "_temperature",
+    "temperature_",
+)
+
+
+def is_printer_coupled(key: str) -> bool:
+    """Whether carrying this process key across printer models is risky."""
+    if key in _PRINTER_COUPLED_EXACT:
+        return True
+    lowered = key.lower()
+    return any(token in lowered for token in _PRINTER_COUPLED_SUBSTRINGS)
+
+
+def _split_changed_keys(entry: Any) -> list[str]:
+    """Parse one ``different_settings_to_system`` entry into its key names."""
+    if not isinstance(entry, str):
+        return []
+    return [part.strip() for part in entry.split(";") if part.strip()]
+
+
+def extract_design_process_overrides(zip_bytes: bytes) -> list[DesignOverride]:
+    """Process settings the 3MF's designer changed away from the system preset.
+
+    Returns an empty list for anything that is not a BambuStudio-style 3MF
+    carrying both ``project_settings.config`` and a well-formed
+    ``different_settings_to_system`` — including OrcaSlicer files and older
+    exports that predate the field. Callers treat empty as "nothing to offer",
+    which is the pre-feature behaviour.
+    """
+    try:
+        with zipfile.ZipFile(BytesIO(zip_bytes), "r") as zf:
+            if _PROJECT_SETTINGS not in zf.namelist():
+                return []
+            config = json.loads(zf.read(_PROJECT_SETTINGS).decode("utf-8"))
+    except (zipfile.BadZipFile, json.JSONDecodeError, UnicodeDecodeError, OSError, KeyError):
+        return []
+    return overrides_from_config(config)
+
+
+def overrides_from_config(config: Any) -> list[DesignOverride]:
+    """``extract_design_process_overrides`` on an already-parsed config dict."""
+    if not isinstance(config, dict):
+        return []
+
+    changed = config.get("different_settings_to_system")
+    if not isinstance(changed, list) or not changed:
+        return []
+
+    # Sanity-check the layout before trusting index 0. The array should be
+    # [process, *filaments, printer]; a file whose length disagrees with its own
+    # filament count is one we do not understand, and guessing there could carry
+    # printer G-code into the process slot.
+    filaments = config.get("filament_settings_id")
+    if isinstance(filaments, list) and len(changed) != len(filaments) + 2:
+        logger.debug(
+            "3MF different_settings_to_system has %d entries for %d filaments "
+            "(expected %d) — skipping design-settings carry-over",
+            len(changed),
+            len(filaments),
+            len(filaments) + 2,
+        )
+        return []
+
+    overrides: list[DesignOverride] = []
+    # Index 0 is the process slot — see the layout in the module docstring. The
+    # length check above is what earns the right to index it blindly.
+    for key in _split_changed_keys(changed[0]):
+        if key not in config:
+            # Listed as changed but absent from the flattened config — nothing
+            # to carry. Seen with keys the slicer renamed between versions.
+            continue
+        overrides.append(DesignOverride(key=key, value=config[key], printer_coupled=is_printer_coupled(key)))
+
+    overrides.sort(key=lambda o: o.key)
+    return overrides
+
+
+def apply_design_overrides(process_json: str, overrides: list[DesignOverride], selected_keys: list[str]) -> str:
+    """Write the selected designer values into the outgoing process JSON.
+
+    ``selected_keys`` is authoritative — a key the caller did not name is not
+    applied even when it is present in ``overrides``. Returns ``process_json``
+    unchanged when nothing is selected or the JSON is unparseable, so a bad
+    input degrades to a plain profile slice rather than failing it.
+    """
+    if not selected_keys or not overrides:
+        return process_json
+
+    wanted = set(selected_keys)
+    by_key = {o.key: o.value for o in overrides if o.key in wanted}
+    if not by_key:
+        return process_json
+
+    try:
+        process_cfg = json.loads(process_json)
+    except json.JSONDecodeError:
+        return process_json
+    if not isinstance(process_cfg, dict):
+        return process_json
+
+    process_cfg.update(by_key)
+    logger.info("Carrying %d design setting(s) onto the picked process preset: %s", len(by_key), sorted(by_key))
+    return json.dumps(process_cfg)

+ 100 - 0
backend/tests/integration/test_design_settings_plates.py

@@ -0,0 +1,100 @@
+"""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"]

+ 191 - 0
backend/tests/unit/test_design_settings.py

@@ -0,0 +1,191 @@
+"""Tests for carrying a 3MF designer's process tweaks across a re-slice (#2622).
+
+The layout asserted here — ``different_settings_to_system`` being
+``[process, *filaments, printer]`` — was verified against real BambuStudio files
+at 2, 3 and 4 filament slots before this was written. Getting the index wrong
+would carry ``machine_start_gcode`` from the designer's printer onto the user's,
+so the parser refuses any file whose array length disagrees with its own filament
+count rather than guessing.
+"""
+
+import io
+import json
+import zipfile
+
+from backend.app.services.design_settings import (
+    DesignOverride,
+    apply_design_overrides,
+    extract_design_process_overrides,
+    is_printer_coupled,
+    overrides_from_config,
+)
+
+
+def _config(**overrides) -> dict:
+    """A minimal project_settings.config with two filament slots."""
+    base = {
+        "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", "Bambu PLA Matte @BBL A1"],
+        "wall_loops": "5",
+        "sparse_infill_density": "100%",
+        "initial_layer_print_height": "0.1",
+        "outer_wall_speed": "200",
+        "machine_start_gcode": "G28 ; designer printer",
+        "different_settings_to_system": [
+            "wall_loops;sparse_infill_density;initial_layer_print_height;outer_wall_speed",
+            "",
+            "",
+            "machine_start_gcode",
+        ],
+    }
+    base.update(overrides)
+    return base
+
+
+def _3mf(config: dict | None, *, include_config: bool = True) -> bytes:
+    buffer = io.BytesIO()
+    with zipfile.ZipFile(buffer, "w") as zf:
+        zf.writestr("3D/3dmodel.model", "<model/>")
+        if include_config:
+            zf.writestr("Metadata/project_settings.config", json.dumps(config))
+    return buffer.getvalue()
+
+
+class TestClassification:
+    def test_geometry_and_quality_keys_are_portable(self):
+        for key in (
+            "wall_loops",
+            "sparse_infill_density",
+            "sparse_infill_pattern",
+            "initial_layer_print_height",
+            "layer_height",
+            "enable_support",
+            "brim_type",
+            "seam_position",
+            "ironing_type",
+        ):
+            assert is_printer_coupled(key) is False, key
+
+    def test_kinematic_and_thermal_keys_are_printer_coupled(self):
+        for key in (
+            "outer_wall_speed",
+            "inner_wall_speed",
+            "internal_solid_infill_speed",
+            "travel_acceleration",
+            "default_acceleration",
+            "default_jerk",
+            "overhang_fan_speed",
+            "nozzle_temperature",
+            "prime_tower_max_speed",
+            "prime_tower_width",
+            "prime_tower_rib_wall",
+            "prime_tower_infill_gap",
+            "enable_prime_tower",
+            "independent_support_layer_height",
+            "precise_z_height",
+        ):
+            assert is_printer_coupled(key) is True, key
+
+
+class TestExtraction:
+    def test_reads_the_process_slot_and_classifies_each_key(self):
+        overrides = extract_design_process_overrides(_3mf(_config()))
+
+        assert [o.key for o in overrides] == [
+            "initial_layer_print_height",
+            "outer_wall_speed",
+            "sparse_infill_density",
+            "wall_loops",
+        ]
+        by_key = {o.key: o for o in overrides}
+        assert by_key["wall_loops"].value == "5"
+        assert by_key["sparse_infill_density"].value == "100%"
+        assert by_key["wall_loops"].printer_coupled is False
+        assert by_key["outer_wall_speed"].printer_coupled is True
+
+    def test_never_surfaces_the_printer_slot(self):
+        # machine_start_gcode is listed in the printer entry, not the process
+        # one. Carrying it would push the designer's start G-code onto another
+        # machine — the exact failure the length check exists to prevent.
+        overrides = extract_design_process_overrides(_3mf(_config()))
+        assert "machine_start_gcode" not in {o.key for o in overrides}
+
+    def test_rejects_an_array_whose_length_contradicts_the_filament_count(self):
+        # Three entries for two filaments: the layout is not the one we know,
+        # so index 0 might not be the process slot. Refuse rather than guess.
+        cfg = _config(different_settings_to_system=["wall_loops", "", ""])
+        assert extract_design_process_overrides(_3mf(cfg)) == []
+
+    def test_skips_keys_absent_from_the_flattened_config(self):
+        cfg = _config(different_settings_to_system=["wall_loops;renamed_in_a_later_slicer", "", "", ""])
+        assert [o.key for o in extract_design_process_overrides(_3mf(cfg))] == ["wall_loops"]
+
+    def test_empty_process_entry_yields_nothing(self):
+        cfg = _config(different_settings_to_system=["", "", "", "machine_start_gcode"])
+        assert extract_design_process_overrides(_3mf(cfg)) == []
+
+    def test_files_without_the_field_yield_nothing(self):
+        cfg = _config()
+        del cfg["different_settings_to_system"]
+        assert extract_design_process_overrides(_3mf(cfg)) == []
+
+    def test_files_without_project_settings_yield_nothing(self):
+        assert extract_design_process_overrides(_3mf(None, include_config=False)) == []
+
+    def test_malformed_input_yields_nothing(self):
+        assert extract_design_process_overrides(b"not a zip") == []
+        assert overrides_from_config("not a dict") == []
+        assert overrides_from_config({"different_settings_to_system": "not a list"}) == []
+
+    def test_tolerates_a_missing_filament_list(self):
+        # No filament_settings_id to cross-check against — index 0 is still the
+        # documented process slot, so parse it rather than bailing out.
+        cfg = _config()
+        del cfg["filament_settings_id"]
+        assert [o.key for o in extract_design_process_overrides(_3mf(cfg))] == [
+            "initial_layer_print_height",
+            "outer_wall_speed",
+            "sparse_infill_density",
+            "wall_loops",
+        ]
+
+
+class TestApply:
+    def _overrides(self) -> list[DesignOverride]:
+        return extract_design_process_overrides(_3mf(_config()))
+
+    def test_writes_only_the_selected_keys(self):
+        process = json.dumps({"inherits": "0.20mm Standard @BBL X1C", "from": "system", "wall_loops": "2"})
+
+        patched = json.loads(apply_design_overrides(process, self._overrides(), ["wall_loops"]))
+
+        assert patched["wall_loops"] == "5"
+        # Not selected — the picked preset's own value must survive.
+        assert "sparse_infill_density" not in patched
+        assert "outer_wall_speed" not in patched
+        # The inherits stub is what makes the patch win over the flattened
+        # parent inside the sidecar; it must not be disturbed.
+        assert patched["inherits"] == "0.20mm Standard @BBL X1C"
+        assert patched["from"] == "system"
+
+    def test_a_key_the_source_never_flagged_is_ignored(self):
+        process = json.dumps({"inherits": "x"})
+
+        patched = json.loads(apply_design_overrides(process, self._overrides(), ["layer_height"]))
+
+        assert "layer_height" not in patched
+
+    def test_printer_coupled_keys_apply_when_explicitly_selected(self):
+        process = json.dumps({"inherits": "x"})
+
+        patched = json.loads(apply_design_overrides(process, self._overrides(), ["outer_wall_speed"]))
+
+        assert patched["outer_wall_speed"] == "200"
+
+    def test_no_selection_is_a_no_op(self):
+        process = json.dumps({"inherits": "x"})
+        assert apply_design_overrides(process, self._overrides(), []) == process
+
+    def test_unparseable_process_json_is_returned_untouched(self):
+        assert apply_design_overrides("{not json", self._overrides(), ["wall_loops"]) == "{not json"

+ 132 - 0
frontend/src/__tests__/components/SliceModal.test.tsx

@@ -332,6 +332,138 @@ describe('SliceModal', () => {
     expect(screen.queryByLabelText(/Use the file's built-in settings/)).toBeNull();
   });
 
+  // #2622: a MakerWorld 3MF designed for another printer carries the author's
+  // own process tweaks. BambuStudio records which keys deviate from the stock
+  // preset inside the file, so a cross-printer re-slice can carry them instead
+  // of losing them to the picked process profile.
+  const designedFor = {
+    file_id: 100,
+    filename: 'Designed.3mf',
+    plates: [],
+    is_multi_plate: false,
+    embedded_printer: 'Bambu Lab A1 0.4 nozzle',
+    embedded_process: '0.20mm Standard @BBL A1',
+    design_overrides: [
+      { key: 'wall_loops', value: '5', printer_coupled: false },
+      { key: 'sparse_infill_density', value: '100%', printer_coupled: false },
+      { key: 'outer_wall_speed', value: '200', printer_coupled: true },
+    ],
+  };
+
+  async function openDesignSection() {
+    const user = userEvent.setup();
+    await user.click(await screen.findByText(/Keep the designer's settings/));
+    return user;
+  }
+
+  it("carries the design's printer-independent settings by default (#2622)", async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+    mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    // Two of three pre-selected: the speed key is machine-coupled.
+    expect(await screen.findByText('2 of 3 selected')).toBeInTheDocument();
+
+    const user = userEvent.setup();
+    await user.click(screen.getByRole('button', { name: /^Slice$/ }));
+
+    await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
+    const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as { design_overrides?: string[] };
+    expect([...(payload.design_overrides ?? [])].sort()).toEqual(['sparse_infill_density', 'wall_loops']);
+  });
+
+  it('lists every changed setting with its value and flags the machine-coupled ones (#2622)', async () => {
+    mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    await openDesignSection();
+
+    expect(screen.getByText('wall_loops')).toBeInTheDocument();
+    expect(screen.getByText('5')).toBeInTheDocument();
+    expect(screen.getByText('sparse_infill_density')).toBeInTheDocument();
+    expect(screen.getByText('100%')).toBeInTheDocument();
+    // The risky one is listed too — visible, explained, just not pre-ticked.
+    expect(screen.getByText('outer_wall_speed')).toBeInTheDocument();
+    expect(screen.getByText('printer-specific')).toBeInTheDocument();
+  });
+
+  it('lets the user opt a machine-coupled setting in and a safe one out (#2622)', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+    mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    const user = await openDesignSection();
+    const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
+    const byKey = (key: string) =>
+      boxes.find((b) => b.closest('label')?.textContent?.includes(key)) as HTMLInputElement;
+
+    await user.click(byKey('outer_wall_speed'));
+    await user.click(byKey('wall_loops'));
+
+    await user.click(screen.getByRole('button', { name: /^Slice$/ }));
+
+    await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
+    const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as { design_overrides?: string[] };
+    expect([...(payload.design_overrides ?? [])].sort()).toEqual(['outer_wall_speed', 'sparse_infill_density']);
+  });
+
+  it('omits design_overrides entirely when the user unticks everything (#2622)', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+    mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    const user = await openDesignSection();
+    const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
+    for (const box of boxes) {
+      if (box.checked) await user.click(box);
+    }
+
+    await user.click(screen.getByRole('button', { name: /^Slice$/ }));
+
+    await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
+    expect(mockApi.sliceLibraryFile.mock.calls[0][1]).not.toHaveProperty('design_overrides');
+  });
+
+  it('hides the section for a file that changes nothing (#2622)', async () => {
+    mockApi.getLibraryFilePlates.mockResolvedValue({ ...designedFor, design_overrides: [] });
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    await waitFor(() => expect(screen.getByRole('button', { name: /^Slice$/ })).toBeEnabled());
+    expect(screen.queryByText(/Keep the designer's settings/)).toBeNull();
+  });
+
   it('includes bed_type in the request when the user picks a non-auto plate (#1337)', async () => {
     const onClose = vi.fn();
     mockApi.sliceLibraryFile.mockResolvedValue({

+ 96 - 1
frontend/src/components/SliceModal.tsx

@@ -16,7 +16,7 @@ import {
 import { useSliceJobTracker } from '../contexts/SliceJobTrackerContext';
 import { useToast } from '../contexts/ToastContext';
 import { PlatePickerModal } from './PlatePickerModal';
-import type { PlateFilament } from '../types/plates';
+import type { DesignOverride, PlateFilament } from '../types/plates';
 import {
   presetCompatibility,
   buildCompatibilityIndex,
@@ -186,6 +186,16 @@ function formatElapsed(seconds: number): string {
   return `${h}h ${remM}m`;
 }
 
+// Render a slicer parameter value for the design-settings list. Bambu's process
+// schema stores everything as strings or arrays of strings, so this only has to
+// flatten arrays and keep scalars readable — no unit or type interpretation,
+// which would rot against every slicer release.
+function formatDesignValue(value: unknown): string {
+  if (Array.isArray(value)) return value.map((v) => String(v)).join(', ');
+  if (value == null) return '';
+  return String(value);
+}
+
 export function SliceModal({ source, onClose }: SliceModalProps) {
   const { t } = useTranslation();
   const { trackJob } = useSliceJobTracker();
@@ -227,6 +237,15 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // see canUseEmbedded below.
   const [useEmbedded, setUseEmbedded] = useState(false);
 
+  // #2622: process settings the designer changed away from the stock preset,
+  // carried onto the picked process profile so a cross-printer re-slice keeps
+  // the model's intended wall count / infill / first layer instead of losing
+  // them to --load-settings. Keys the file flags as machine-coupled (speeds,
+  // accelerations, prime-tower geometry) are listed but start unticked — those
+  // were tuned for the designer's printer and can be plain wrong on another.
+  const [designKeys, setDesignKeys] = useState<Set<string>>(new Set());
+  const [designExpanded, setDesignExpanded] = useState(false);
+
   // Slicer Pipelines (#1425) — apply a saved preset bundle to all four slots
   // with one pick, or save the current selection as a new pipeline.
   const pipelinesQuery = useQuery({
@@ -385,6 +404,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // plates query resolves before the presets query (the latter is gated on
   // it), so these are known by the time the pre-pick effects run.
   const embeddedPrinter = platesQuery.data?.embedded_printer ?? null;
+  const designOverrides = useMemo<DesignOverride[]>(
+    () => platesQuery.data?.design_overrides ?? [],
+    [platesQuery.data],
+  );
   const embeddedProcess = platesQuery.data?.embedded_process ?? null;
 
   // "Slice as designed" is offered only when the source carries embedded
@@ -405,6 +428,12 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
     if (!canUseEmbedded) setUseEmbedded(false);
   }, [canUseEmbedded]);
 
+  // Pre-tick the printer-independent design settings once the source's list
+  // arrives. Machine-coupled keys stay off until the user opts in explicitly.
+  useEffect(() => {
+    setDesignKeys(new Set(designOverrides.filter((o) => !o.printer_coupled).map((o) => o.key)));
+  }, [designOverrides]);
+
   // Printer pre-pick: defaults to the printer the 3MF was prepared for when
   // that preset is available, else the first listed printer. Runs once when
   // presets first arrive; later re-renders preserve any manual choice.
@@ -505,6 +534,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
       // them) but go unused when this flag is set — the slicer falls back on
       // the file's embedded project_settings.config instead.
       ...(useEmbedded && canUseEmbedded ? { use_embedded_settings: true } : {}),
+      // Carried design settings are patched onto the resolved process JSON,
+      // which the embedded-settings path never sends — so they are mutually
+      // exclusive by construction (#2622).
+      ...(!useEmbedded && designKeys.size > 0 ? { design_overrides: [...designKeys] } : {}),
     };
   }
 
@@ -775,6 +808,68 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                 selectedPrinterName={selectedPrinterName}
                 compatIndex={compatIndex}
               />
+              {/* Designer's process tweaks (#2622). BambuStudio records which
+                  keys deviate from the stock preset in the 3MF itself, so a
+                  re-slice for another printer can carry them instead of
+                  flattening them under --load-settings. Hidden entirely when
+                  the source lists none, and disabled in embedded mode where
+                  the process JSON these patch is never sent. */}
+              {designOverrides.length > 0 && (
+                <div className="rounded-lg border border-bambu-dark-tertiary bg-bambu-dark/40 p-3">
+                  <button
+                    type="button"
+                    onClick={() => setDesignExpanded((v) => !v)}
+                    className="flex w-full items-center justify-between gap-2 text-left"
+                  >
+                    <span className="text-sm text-white">
+                      {t('slice.designSettings')}
+                      <span className="block text-xs text-bambu-gray/70">
+                        {t('slice.designSettingsHint', { count: designOverrides.length })}
+                      </span>
+                    </span>
+                    <span className="shrink-0 text-xs text-bambu-gray">
+                      {t('slice.designSettingsSelected', { selected: designKeys.size, total: designOverrides.length })}
+                    </span>
+                  </button>
+                  {designExpanded && (
+                    <div className="mt-3 space-y-1.5 border-t border-bambu-dark-tertiary pt-3">
+                      {designOverrides.map((o) => (
+                        <label
+                          key={o.key}
+                          className={`flex items-start gap-2 text-xs ${useEmbedded ? 'opacity-50' : 'cursor-pointer'}`}
+                        >
+                          <input
+                            type="checkbox"
+                            checked={designKeys.has(o.key)}
+                            disabled={isEnqueuing || useEmbedded}
+                            onChange={(e) => {
+                              setDesignKeys((prev) => {
+                                const next = new Set(prev);
+                                if (e.target.checked) next.add(o.key);
+                                else next.delete(o.key);
+                                return next;
+                              });
+                            }}
+                            className="mt-0.5 shrink-0 cursor-pointer"
+                          />
+                          <span className="min-w-0 flex-1">
+                            <span className="font-mono text-bambu-gray">{o.key}</span>
+                            <span className="ml-1.5 break-all text-white">{formatDesignValue(o.value)}</span>
+                            {o.printer_coupled && (
+                              <span
+                                className="ml-1.5 rounded bg-amber-100 px-1 py-0.5 text-[10px] text-amber-700 dark:bg-amber-500/20 dark:text-amber-400"
+                                title={t('slice.designSettingsPrinterCoupledHint')}
+                              >
+                                {t('slice.designSettingsPrinterCoupled')}
+                              </span>
+                            )}
+                          </span>
+                        </label>
+                      ))}
+                    </div>
+                  )}
+                </div>
+              )}
               {/* Bed-type override (#1337). Always visible, always enabled.
                   The backend patches curr_bed_type on the resolved process
                   JSON before forwarding to the sidecar. */}

+ 5 - 0
frontend/src/i18n/locales/de.ts

@@ -4072,6 +4072,11 @@ export default {
     allPresetsRequired: 'Alle Profile müssen ausgewählt sein',
     useEmbedded: 'Eingebettete Einstellungen der Datei verwenden',
     useEmbeddedHint: 'So slicen, wie der Ersteller es angelegt hat (Wände, Füllung, Filament), statt mit den obigen Profilen. Verfügbar, weil dein Drucker zur Datei passt.',
+    designSettings: 'Einstellungen des Erstellers behalten',
+    designSettingsHint: 'Diese Datei ändert {{count}} Druckeinstellung(en) gegenüber dem Standardprofil.',
+    designSettingsSelected: '{{selected}} von {{total}} ausgewählt',
+    designSettingsPrinterCoupled: 'druckerspezifisch',
+    designSettingsPrinterCoupledHint: 'Auf den Drucker abgestimmt, für den die Datei erstellt wurde — auf deinem kann der Wert falsch oder unzulässig sein.',
     enqueuing: 'Slice-Auftrag wird übermittelt…',
     queued: 'In Warteschlange…',
     failed: 'Slicen fehlgeschlagen. Logs des Slicer-Sidecars prüfen.',

+ 5 - 0
frontend/src/i18n/locales/en.ts

@@ -4106,6 +4106,11 @@ export default {
     allPresetsRequired: 'All presets must be selected',
     useEmbedded: "Use the file's built-in settings",
     useEmbeddedHint: "Slice it the way the designer set it up (walls, infill, filament) instead of the profiles above. Offered because your printer matches the file's.",
+    designSettings: 'Keep the designer\'s settings',
+    designSettingsHint: 'This file changes {{count}} print setting(s) from the stock profile.',
+    designSettingsSelected: '{{selected}} of {{total}} selected',
+    designSettingsPrinterCoupled: 'printer-specific',
+    designSettingsPrinterCoupledHint: 'Tuned for the printer this file was designed for — may be wrong or out of range on yours.',
     enqueuing: 'Submitting slice job…',
     queued: 'Queued…',
     failed: 'Slicing failed. Check the slicer sidecar logs.',

+ 5 - 0
frontend/src/i18n/locales/es.ts

@@ -4075,6 +4075,11 @@ export default {
     allPresetsRequired: 'Deben seleccionarse todos los preajustes',
     useEmbedded: 'Usar la configuración incorporada del archivo',
     useEmbeddedHint: 'Laminar tal como lo configuró el diseñador (perímetros, relleno, filamento) en lugar de los perfiles de arriba. Disponible porque tu impresora coincide con la del archivo.',
+    designSettings: 'Mantener los ajustes del diseñador',
+    designSettingsHint: 'Este archivo cambia {{count}} ajuste(s) de impresión respecto al perfil estándar.',
+    designSettingsSelected: '{{selected}} de {{total}} seleccionados',
+    designSettingsPrinterCoupled: 'específico de la impresora',
+    designSettingsPrinterCoupledHint: 'Ajustado para la impresora para la que se diseñó el archivo: puede ser incorrecto o quedar fuera de rango en la tuya.',
     enqueuing: 'Enviando el trabajo de laminado…',
     queued: 'En cola…',
     failed: 'Error al laminar. Consulte los registros del contenedor auxiliar del laminador.',

+ 5 - 0
frontend/src/i18n/locales/fr.ts

@@ -4061,6 +4061,11 @@ export default {
     allPresetsRequired: 'Tous les préréglages doivent être sélectionnés',
     useEmbedded: 'Utiliser les réglages intégrés du fichier',
     useEmbeddedHint: "Slicer tel que le concepteur l'a configuré (parois, remplissage, filament) au lieu des profils ci-dessus. Proposé car votre imprimante correspond à celle du fichier.",
+    designSettings: 'Conserver les réglages du concepteur',
+    designSettingsHint: 'Ce fichier modifie {{count}} réglage(s) d\'impression par rapport au profil standard.',
+    designSettingsSelected: '{{selected}} sur {{total}} sélectionnés',
+    designSettingsPrinterCoupled: 'spécifique à l\'imprimante',
+    designSettingsPrinterCoupledHint: 'Réglé pour l\'imprimante visée par le fichier — peut être incorrect ou hors plage sur la vôtre.',
     enqueuing: 'Envoi du travail de découpage…',
     queued: 'En file d\'attente…',
     failed: 'Échec du découpage. Vérifiez les journaux du sidecar.',

+ 5 - 0
frontend/src/i18n/locales/it.ts

@@ -4060,6 +4060,11 @@ export default {
     allPresetsRequired: 'Tutti i preset devono essere selezionati',
     useEmbedded: 'Usa le impostazioni integrate del file',
     useEmbeddedHint: 'Slicia come impostato dal designer (pareti, riempimento, filamento) invece dei profili sopra. Disponibile perché la tua stampante corrisponde a quella del file.',
+    designSettings: 'Mantieni le impostazioni del progettista',
+    designSettingsHint: 'Questo file modifica {{count}} impostazione/i di stampa rispetto al profilo standard.',
+    designSettingsSelected: '{{selected}} di {{total}} selezionate',
+    designSettingsPrinterCoupled: 'specifico della stampante',
+    designSettingsPrinterCoupledHint: 'Tarato per la stampante per cui è stato progettato il file: sulla tua può essere errato o fuori intervallo.',
     enqueuing: 'Invio lavoro di slicing…',
     queued: 'In coda…',
     failed: 'Slicing fallito. Controlla i log del sidecar.',

+ 5 - 0
frontend/src/i18n/locales/ja.ts

@@ -4072,6 +4072,11 @@ export default {
     allPresetsRequired: 'すべてのプリセットを選択する必要があります',
     useEmbedded: 'ファイルに埋め込まれた設定を使用',
     useEmbeddedHint: '上のプロファイルではなく、設計者が設定したとおり(ウォール、インフィル、フィラメント)にスライスします。お使いのプリンターがファイルと一致するため利用できます。',
+    designSettings: '設計者の設定を保持',
+    designSettingsHint: 'このファイルは標準プロファイルから {{count}} 個の印刷設定を変更しています。',
+    designSettingsSelected: '{{total}} 個中 {{selected}} 個を選択',
+    designSettingsPrinterCoupled: 'プリンター固有',
+    designSettingsPrinterCoupledHint: 'このファイルが対象とするプリンター向けに調整された値です。お使いのプリンターでは不適切または範囲外になる場合があります。',
     enqueuing: 'スライスジョブを送信中…',
     queued: '待機中…',
     failed: 'スライスに失敗。サイドカーのログを確認してください。',

+ 5 - 0
frontend/src/i18n/locales/ko.ts

@@ -3861,6 +3861,11 @@ export default {
     allPresetsRequired: '모든 프리셋을 선택해야 합니다',
     useEmbedded: '파일에 포함된 설정 사용',
     useEmbeddedHint: '위 프로필 대신 디자이너가 설정한 대로(벽, 내부 채움, 필라멘트) 슬라이싱합니다. 프린터가 파일과 일치하여 사용할 수 있습니다.',
+    designSettings: '디자이너 설정 유지',
+    designSettingsHint: '이 파일은 기본 프로파일에서 {{count}}개의 출력 설정을 변경합니다.',
+    designSettingsSelected: '{{total}}개 중 {{selected}}개 선택됨',
+    designSettingsPrinterCoupled: '프린터 전용',
+    designSettingsPrinterCoupledHint: '이 파일이 대상으로 한 프린터에 맞춘 값입니다. 사용 중인 프린터에서는 잘못되었거나 범위를 벗어날 수 있습니다.',
     enqueuing: '슬라이싱 작업 제출 중…',
     queued: '대기 중…',
     failed: '슬라이싱 실패. 슬라이서 사이드카 로그를 확인하세요.',

+ 5 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -4060,6 +4060,11 @@ export default {
     allPresetsRequired: 'Todas as predefinições devem ser selecionadas',
     useEmbedded: 'Usar as configurações incorporadas do arquivo',
     useEmbeddedHint: 'Fatiar como o designer configurou (paredes, preenchimento, filamento) em vez dos perfis acima. Disponível porque sua impressora corresponde à do arquivo.',
+    designSettings: 'Manter as configurações do designer',
+    designSettingsHint: 'Este arquivo altera {{count}} configuração(ões) de impressão em relação ao perfil padrão.',
+    designSettingsSelected: '{{selected}} de {{total}} selecionadas',
+    designSettingsPrinterCoupled: 'específico da impressora',
+    designSettingsPrinterCoupledHint: 'Ajustado para a impressora para a qual o arquivo foi projetado — pode estar incorreto ou fora de faixa na sua.',
     enqueuing: 'Enviando trabalho de fatiamento…',
     queued: 'Na fila…',
     failed: 'Falha ao fatiar. Verifique os logs do sidecar.',

+ 5 - 0
frontend/src/i18n/locales/ru.ts

@@ -3857,6 +3857,11 @@ export default {
     allPresetsRequired: "Необходимо выбрать все профили",
     useEmbedded: "Использовать встроенные настройки файла",
     useEmbeddedHint: "Нарезать так, как задумал автор модели (стенки, заполнение, филамент), вместо профилей выше. Доступно, потому что ваш принтер совпадает с указанным в файле.",
+    designSettings: "Сохранить настройки автора",
+    designSettingsHint: "Этот файл меняет {{count}} настроек печати по сравнению со стандартным профилем.",
+    designSettingsSelected: "Выбрано {{selected}} из {{total}}",
+    designSettingsPrinterCoupled: "зависит от принтера",
+    designSettingsPrinterCoupledHint: "Значение подобрано для принтера, под который создан файл, — на вашем оно может быть неверным или вне допустимого диапазона.",
     enqueuing: "Отправка задания на нарезку…",
     queued: "В очереди…",
     failed: "Ошибка нарезки. Проверьте журналы вспомогательного сервиса слайсера.",

+ 5 - 0
frontend/src/i18n/locales/tr.ts

@@ -4062,6 +4062,11 @@ export default {
     allPresetsRequired: 'Tüm ön ayarlar seçilmelidir',
     useEmbedded: 'Dosyanın yerleşik ayarlarını kullan',
     useEmbeddedHint: 'Yukarıdaki profiller yerine tasarımcının ayarladığı gibi (duvarlar, dolgu, filament) dilimle. Yazıcınız dosyayla eşleştiği için sunuluyor.',
+    designSettings: 'Tasarımcının ayarlarını koru',
+    designSettingsHint: 'Bu dosya standart profile göre {{count}} baskı ayarını değiştiriyor.',
+    designSettingsSelected: '{{total}} ayardan {{selected}} tanesi seçili',
+    designSettingsPrinterCoupled: 'yazıcıya özel',
+    designSettingsPrinterCoupledHint: 'Dosyanın tasarlandığı yazıcıya göre ayarlanmış — sizinkinde yanlış veya aralık dışı olabilir.',
     enqueuing: 'Dilimleme işi gönderiliyor…',
     queued: 'Kuyrukta…',
     failed: 'Dilimleme başarısız. Dilimleyici yardımcı bileşen günlüklerini kontrol edin.',

+ 5 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -4060,6 +4060,11 @@ export default {
     allPresetsRequired: '必须选择所有预设',
     useEmbedded: '使用文件的内置设置',
     useEmbeddedHint: '按设计者的设置(壁、填充、耗材)切片,而非上方的配置文件。因您的打印机与文件匹配而可用。',
+    designSettings: '保留设计者的设置',
+    designSettingsHint: '此文件相对标准配置修改了 {{count}} 项打印设置。',
+    designSettingsSelected: '已选择 {{selected}} / {{total}}',
+    designSettingsPrinterCoupled: '与打印机相关',
+    designSettingsPrinterCoupledHint: '该值是为此文件面向的打印机调校的,在你的打印机上可能不正确或超出范围。',
     enqueuing: '提交切片任务中…',
     queued: '已排队…',
     failed: '切片失败。请检查切片器 sidecar 日志。',

+ 5 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -4060,6 +4060,11 @@ export default {
     allPresetsRequired: '必須選擇所有預設',
     useEmbedded: '使用檔案的內建設定',
     useEmbeddedHint: '依設計者的設定(外牆、填充、耗材)切片,而非上方的設定檔。因您的印表機與檔案相符而可用。',
+    designSettings: '保留設計者的設定',
+    designSettingsHint: '此檔案相對標準設定檔修改了 {{count}} 項列印設定。',
+    designSettingsSelected: '已選擇 {{selected}} / {{total}}',
+    designSettingsPrinterCoupled: '與印表機相關',
+    designSettingsPrinterCoupledHint: '此值是為該檔案面向的印表機調校的,在你的印表機上可能不正確或超出範圍。',
     enqueuing: '提交切片任務中…',
     queued: '已排隊…',
     failed: '切片失敗。請檢查切片器 sidecar 日誌。',

+ 15 - 0
frontend/src/types/plates.ts

@@ -38,6 +38,21 @@ export interface PlateMetadata {
 interface EmbeddedPresets {
   embedded_printer?: string | null;
   embedded_process?: string | null;
+  // Process settings the designer changed away from the stock preset, read
+  // from the 3MF's own `different_settings_to_system` (#2622). Offered in the
+  // SliceModal so a re-slice for another printer can carry them instead of
+  // losing them to the picked process profile. Empty for STL, OrcaSlicer
+  // files, and older exports that predate the field.
+  design_overrides?: DesignOverride[];
+}
+
+// One process setting the designer deviated on. `printer_coupled` marks the
+// values that only make sense on the machine they were tuned for (speeds,
+// accelerations, prime-tower geometry) — offered, but never pre-selected.
+export interface DesignOverride {
+  key: string;
+  value: unknown;
+  printer_coupled: boolean;
 }
 
 export interface ArchivePlatesResponse extends EmbeddedPresets {

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-D5mCVall.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-JUS5PFis.js"></script>
+    <script type="module" crossorigin src="/assets/index-D5mCVall.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-D4bpNaiw.css">
   </head>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов