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

Add auto-orient and auto-arrange to server-side slicing (#2548)

Both are per-slice checkboxes, off by default, forwarded as the sidecar's
orient / arrange form fields. An unticked box is sent by omission: the
sidecar treats any present value as truthy, so a literal "false" would
have arranged every slice.

Arrange unions with the #1493 cross-class decision rather than replacing
it, and the per-plate slice-all loop is now keyed on the arrange flag
itself — the project-wide collapse belongs to --arrange, not to the
cross-class case. The loop also covers the embedded-settings path, whose
crash-retry is suppressed there since a single --slice 0 retry would
return one consolidated plate.
maziggy 1 месяц назад
Родитель
Сommit
e95c42c021

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 95 - 41
backend/app/api/routes/library.py

@@ -3772,6 +3772,13 @@ async def _run_slicer_with_fallback(
                 target_model,
             )
             cross_class_arrange = True
+
+    # #2548: the user can also ask for either layout pass per-slice. Arrange
+    # is a union with the cross-class decision above — a user opt-out must
+    # not be able to switch off the flag that keeps a class-crossing slice
+    # from crashing — while orient is user-driven only.
+    arrange_flag = cross_class_arrange or request.auto_arrange
+    orient_flag = request.auto_orient
     # When this slice is dispatcher-tracked, generate a request_id so
     # the sidecar publishes progress under it, and wire a callback that
     # forwards each frame onto SliceDispatchService.set_progress for the
@@ -3821,36 +3828,27 @@ async def _run_slicer_with_fallback(
 
         filament_jsons = substitute_unused_plate_filaments(primary_bytes, request.plate or 1, filament_jsons)
 
-    # Cross-class slice-all loop (#1493): when the user asks for
-    # ``plate=0`` (all plates) AND the source's nozzle class differs from
-    # the target's, ``--slice 0 --arrange 1`` consolidates every plate's
-    # objects onto a single target bed (BS's ``--arrange`` is project-
-    # wide) — either packing them all together or rejecting with "Some
-    # objects are located over the boundary of the heated bed" when
-    # nothing fits. Slice each plate independently with ``--arrange 1``
-    # and merge the per-plate outputs into one multi-plate 3MF instead.
-    # Same-class slice-all goes through the regular path below — the
-    # sidecar's native ``--slice 0`` produces the right shape directly.
-    use_cross_class_slice_all = cross_class_arrange and request.plate == 0 and request.export_3mf
+    # Arrange slice-all loop (#1493): when the user asks for ``plate=0``
+    # (all plates) AND arrange is on, ``--slice 0 --arrange 1``
+    # consolidates every plate's objects onto a single target bed (BS's
+    # ``--arrange`` is project-wide) — either packing them all together or
+    # rejecting with "Some objects are located over the boundary of the
+    # heated bed" when nothing fits. Slice each plate independently with
+    # ``--arrange 1`` and merge the per-plate outputs into one multi-plate
+    # 3MF instead. Slice-all without arrange goes through the regular path
+    # below — the sidecar's native ``--slice 0`` produces the right shape
+    # directly.
+    #
+    # Keyed on ``arrange_flag``, not just the cross-class decision: the
+    # project-wide collapse is a property of ``--arrange`` itself, so a
+    # user-requested arrange over all plates (#2548) hits it identically.
+    # Orient doesn't — it rotates objects where they stand and never moves
+    # one between plates — so it isn't part of this condition.
+    use_arrange_slice_all = arrange_flag and request.plate == 0 and request.export_3mf
 
     try:
         try:
-            if embedded_mode:
-                # No --load-settings: feed the CLI the file's own
-                # project_settings.config untouched so the designer's tweaks
-                # (walls, infill, etc.) drive the slice. primary_bytes is
-                # already sentinel-sanitised above, the same bytes the
-                # crash-fallback uses. The resolved presets go unused here.
-                result = await service.slice_without_profiles(
-                    model_bytes=primary_bytes,
-                    model_filename=model_filename,
-                    plate=request.plate,
-                    export_3mf=request.export_3mf,
-                    request_id=progress_request_id,
-                    on_progress=progress_callback,
-                )
-                used_embedded_settings = True
-            elif use_cross_class_slice_all:
+            if use_arrange_slice_all:
                 from backend.app.services.slicer_3mf_convert import (
                     count_plates_in_3mf,
                     merge_plate_3mfs,
@@ -3867,8 +3865,10 @@ async def _run_slicer_with_fallback(
                         ),
                     )
                 logger.info(
-                    "Cross-class slice-all: looping over %d plates with --arrange per plate, then merging",
+                    "Arrange slice-all: looping over %d plates with --arrange per plate, then merging "
+                    "(embedded_settings=%s)",
                     plate_count,
+                    embedded_mode,
                 )
                 from backend.app.services.slicer_api import SliceResult
 
@@ -3897,18 +3897,35 @@ async def _run_slicer_with_fallback(
 
                 for plate_num in range(1, plate_count + 1):
                     plate_cb = _wrap_progress_for_plate(plate_num, plate_count)
-                    per_plate = await service.slice_with_profiles(
-                        model_bytes=primary_bytes,
-                        model_filename=model_filename,
-                        printer_profile_json=presets["printer"],
-                        process_profile_json=presets["process"],
-                        filament_profile_jsons=filament_jsons,
-                        plate=plate_num,
-                        export_3mf=True,
-                        arrange=True,
-                        request_id=progress_request_id,
-                        on_progress=plate_cb,
-                    )
+                    # "Slice as designed" has to take the loop too, not skip
+                    # it: the project-wide collapse is caused by --arrange,
+                    # and which config drives the slice has no bearing on
+                    # that. Same call, minus --load-settings.
+                    if embedded_mode:
+                        per_plate = await service.slice_without_profiles(
+                            model_bytes=primary_bytes,
+                            model_filename=model_filename,
+                            plate=plate_num,
+                            export_3mf=True,
+                            arrange=True,
+                            orient=orient_flag,
+                            request_id=progress_request_id,
+                            on_progress=plate_cb,
+                        )
+                    else:
+                        per_plate = await service.slice_with_profiles(
+                            model_bytes=primary_bytes,
+                            model_filename=model_filename,
+                            printer_profile_json=presets["printer"],
+                            process_profile_json=presets["process"],
+                            filament_profile_jsons=filament_jsons,
+                            plate=plate_num,
+                            export_3mf=True,
+                            arrange=True,
+                            orient=orient_flag,
+                            request_id=progress_request_id,
+                            on_progress=plate_cb,
+                        )
                     per_plate_results.append((plate_num, per_plate))
 
                 # Merge the N single-plate 3MFs into one multi-plate 3MF.
@@ -3929,6 +3946,28 @@ async def _run_slicer_with_fallback(
                     filament_used_g=sum(r.filament_used_g for _, r in per_plate_results),
                     filament_used_mm=sum(r.filament_used_mm for _, r in per_plate_results),
                 )
+                # Report the path honestly: the loop can run either way, and
+                # the UI reads this flag to tell the user whose settings won.
+                used_embedded_settings = embedded_mode
+            elif embedded_mode:
+                # No --load-settings: feed the CLI the file's own
+                # project_settings.config untouched so the designer's tweaks
+                # (walls, infill, etc.) drive the slice. primary_bytes is
+                # already sentinel-sanitised above, the same bytes the
+                # crash-fallback uses. The resolved presets go unused here.
+                # Arrange / orient still apply: they are CLI actions on the
+                # geometry, not settings the embedded config could carry.
+                result = await service.slice_without_profiles(
+                    model_bytes=primary_bytes,
+                    model_filename=model_filename,
+                    plate=request.plate,
+                    export_3mf=request.export_3mf,
+                    arrange=arrange_flag,
+                    orient=orient_flag,
+                    request_id=progress_request_id,
+                    on_progress=progress_callback,
+                )
+                used_embedded_settings = True
             else:
                 result = await service.slice_with_profiles(
                     model_bytes=primary_bytes,
@@ -3938,7 +3977,8 @@ async def _run_slicer_with_fallback(
                     filament_profile_jsons=filament_jsons,
                     plate=request.plate,
                     export_3mf=request.export_3mf,
-                    arrange=cross_class_arrange,
+                    arrange=arrange_flag,
+                    orient=orient_flag,
                     request_id=progress_request_id,
                     on_progress=progress_callback,
                 )
@@ -3958,6 +3998,14 @@ async def _run_slicer_with_fallback(
                 # error (the outer handler turns it into a 502) instead of
                 # re-running the same embedded slice.
                 raise
+            if use_arrange_slice_all:
+                # The fallback is a single ``--slice 0`` call, and with
+                # arrange on that collapses every plate onto one bed — the
+                # exact outcome the per-plate loop above exists to avoid.
+                # Retrying would hand back a one-plate result for a job the
+                # user asked to slice as N, which reads as a Bambuddy bug
+                # rather than a slicer failure. Surface the error instead.
+                raise
             logger.warning(
                 "Slicer CLI failed on the --load-settings path for %s (%s); retrying with embedded settings",
                 model_filename,
@@ -3971,11 +4019,17 @@ async def _run_slicer_with_fallback(
             # there too, so without sanitisation the fallback would die
             # on the same sentinel error (#1201). The SliceModal flags
             # the difference to the user via used_embedded_settings.
+            # Carry the layout flags across too — the retry is meant to
+            # differ from the failed attempt only in where the print
+            # config came from, so dropping them here would silently
+            # produce an un-arranged result the user did ask for.
             result = await service.slice_without_profiles(
                 model_bytes=primary_bytes,
                 model_filename=model_filename,
                 plate=request.plate,
                 export_3mf=request.export_3mf,
+                arrange=arrange_flag,
+                orient=orient_flag,
                 request_id=progress_request_id,
                 on_progress=progress_callback,
             )

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

@@ -119,6 +119,27 @@ class SliceRequest(BaseModel):
             "process preset unchanged (#1337)."
         ),
     )
+    auto_orient: bool = Field(
+        default=False,
+        description=(
+            "Let the slicer pick each object's orientation before slicing "
+            "(BambuStudio / OrcaSlicer ``--orient 1``, the GUI's 'Auto orient'). "
+            "Off by default: it rotates geometry, so a model the designer laid "
+            "flat on purpose would silently change. Applies on the embedded-"
+            "settings path too — it is a CLI action, not a profile value (#2548)."
+        ),
+    )
+    auto_arrange: bool = Field(
+        default=False,
+        description=(
+            "Let the slicer lay the objects out on the plate before slicing "
+            "(``--arrange 1``, the GUI's 'Auto arrange'). Off by default: it "
+            "repositions objects, discarding a deliberate layout. Forced on "
+            "regardless for cross-nozzle-class re-slices, where the source's "
+            "coordinates land in the target's dead zone (#1493). Applies on the "
+            "embedded-settings path too (#2548)."
+        ),
+    )
 
     @model_validator(mode="after")
     def normalise_preset_refs(self) -> "SliceRequest":

+ 39 - 6
backend/app/services/slicer_api.py

@@ -471,6 +471,7 @@ class SlicerApiService:
         plate: int | None = None,
         export_3mf: bool = False,
         arrange: bool = False,
+        orient: bool = False,
         request_id: str | None = None,
         on_progress: Callable[[dict], None] | None = None,
     ) -> SliceResult:
@@ -489,7 +490,15 @@ class SlicerApiService:
         the source's X1C-coordinate layout would otherwise drop into an H2D
         dead zone or trigger the multi-extruder geometry pipeline's polygon
         clipping crash. Default off so single-printer slices preserve the
-        user's deliberate layout.
+        user's deliberate layout. Also settable per-slice by the user
+        (#2548).
+
+        ``orient`` forwards ``--orient``, the CLI's auto-orientation pass:
+        the slicer scores candidate rotations (overhang area, contour,
+        unprintability) and rotates each object onto the best one before
+        slicing. User-driven only — nothing in Bambuddy turns it on by
+        itself, since rotating a deliberately-laid-out model is not a
+        change to make silently.
 
         ``request_id``: when supplied, the sidecar wires --pipe to a
         per-request FIFO and publishes structured JSON progress events to
@@ -522,11 +531,7 @@ class SlicerApiService:
             data["plate"] = str(plate)
         if export_3mf:
             data["exportType"] = "3mf"
-        if arrange:
-            # Sidecar reads non-empty truthy strings as True; only send the
-            # field when we want the flag on, so default-off callers exactly
-            # match the previous wire payload.
-            data["arrange"] = "true"
+        _add_layout_flags(data, arrange=arrange, orient=orient)
         if request_id is not None:
             data["requestId"] = request_id
 
@@ -545,6 +550,8 @@ class SlicerApiService:
         model_filename: str,
         plate: int | None = None,
         export_3mf: bool = False,
+        arrange: bool = False,
+        orient: bool = False,
         request_id: str | None = None,
         on_progress: Callable[[dict], None] | None = None,
     ) -> SliceResult:
@@ -563,6 +570,14 @@ class SlicerApiService:
         events to the ProgressStore so the modal's inline spinner +
         toast can show "Generating G-code (75%)" for that preview as
         well.
+
+        ``arrange`` / ``orient`` mean the same as on
+        ``slice_with_profiles``: they are CLI actions applied to the loaded
+        geometry, independent of where the print config came from. Both
+        paths accept them so a user's per-slice choice survives the
+        embedded-settings route and the segfault fallback — the filament-
+        discovery preview leaves them off, since moving objects there
+        would change nothing about which slots the plate consumes.
         """
         files = {
             "file": (model_filename, model_bytes, _guess_model_content_type(model_filename)),
@@ -572,6 +587,7 @@ class SlicerApiService:
             data["plate"] = str(plate)
         if export_3mf:
             data["exportType"] = "3mf"
+        _add_layout_flags(data, arrange=arrange, orient=orient)
         if request_id is not None:
             data["requestId"] = request_id
 
@@ -584,6 +600,23 @@ class SlicerApiService:
         return _handle_slice_response(response, export_3mf=export_3mf)
 
 
+def _add_layout_flags(data: dict[str, str], *, arrange: bool, orient: bool) -> None:
+    """Set the sidecar's ``arrange`` / ``orient`` form fields, but only when on.
+
+    The sidecar branches on ``settings.arrange !== undefined`` and forwards
+    ``--arrange 1`` / ``--arrange 0`` accordingly — but multipart fields
+    arrive as *strings*, and ``"false"`` is truthy in JavaScript. Sending
+    ``"false"`` would therefore turn the flag ON. So an off flag is
+    expressed by omitting the field entirely, which also keeps the wire
+    payload of default-off callers byte-identical to before these
+    parameters existed.
+    """
+    if arrange:
+        data["arrange"] = "true"
+    if orient:
+        data["orient"] = "true"
+
+
 def _safe_int(value: str | None) -> int:
     if not value:
         return 0

+ 354 - 0
backend/tests/integration/test_library_slice_api.py

@@ -334,6 +334,82 @@ class TestSliceLibraryFile:
             "bed_type must stay out of the process JSON when no override is set"
         )
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_auto_orient_and_arrange_reach_the_sidecar(self, async_client: AsyncClient, slice_test_setup):
+        """#2548: the two layout passes are per-slice options, so ticking
+        them in the SliceModal has to come out the other end as the
+        sidecar's ``orient`` / ``arrange`` form fields. Before this the
+        flags existed on the wire but only #1493's cross-class detector
+        could set arrange, and nothing at all could set orient."""
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            captured["body"] = bytes(request.content)
+            return httpx.Response(
+                status_code=200,
+                content=_make_3mf_with_settings(),
+                headers={
+                    "x-print-time-seconds": "10",
+                    "x-filament-used-g": "0.1",
+                    "x-filament-used-mm": "1.0",
+                },
+            )
+
+        _install_mock_sidecar(handler)
+        response = await async_client.post(
+            f"/api/v1/library/files/{slice_test_setup['src_file_id']}/slice",
+            json={
+                "printer_preset_id": slice_test_setup["printer_id"],
+                "process_preset_id": slice_test_setup["process_id"],
+                "filament_preset_id": slice_test_setup["filament_id"],
+                "auto_orient": True,
+                "auto_arrange": True,
+            },
+        )
+        assert response.status_code == 202
+        final = await _wait_for_job(async_client, response.json()["job_id"])
+        assert final["status"] == "completed", final
+
+        assert b'name="orient"' in captured["body"]
+        assert b'name="arrange"' in captured["body"]
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_layout_flags_absent_by_default(self, async_client: AsyncClient, slice_test_setup):
+        """Companion to the above. Both default to off, and off is expressed
+        by omitting the field — the sidecar reads any present value as
+        truthy, so a "false" on the wire would auto-arrange every slice."""
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            captured["body"] = bytes(request.content)
+            return httpx.Response(
+                status_code=200,
+                content=_make_3mf_with_settings(),
+                headers={
+                    "x-print-time-seconds": "10",
+                    "x-filament-used-g": "0.1",
+                    "x-filament-used-mm": "1.0",
+                },
+            )
+
+        _install_mock_sidecar(handler)
+        response = await async_client.post(
+            f"/api/v1/library/files/{slice_test_setup['src_file_id']}/slice",
+            json={
+                "printer_preset_id": slice_test_setup["printer_id"],
+                "process_preset_id": slice_test_setup["process_id"],
+                "filament_preset_id": slice_test_setup["filament_id"],
+            },
+        )
+        assert response.status_code == 202
+        final = await _wait_for_job(async_client, response.json()["job_id"])
+        assert final["status"] == "completed", final
+
+        assert b'name="orient"' not in captured["body"]
+        assert b'name="arrange"' not in captured["body"]
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_invalid_preset_id_surfaces_as_failed_job_with_status_400(
@@ -864,6 +940,284 @@ class TestCrossClassSliceAllLoop:
         assert new_archive.print_time_seconds == 600 * 3
         assert new_archive.filament_used_grams == pytest.approx(5.0 * 3)
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_user_requested_arrange_also_loops_per_plate(
+        self, async_client: AsyncClient, db_session, slice_test_setup, printer_factory, archive_factory, monkeypatch
+    ):
+        """#2548 inherits #1493's hazard. The per-plate loop exists because
+        ``--arrange`` is project-wide: a single ``--slice 0 --arrange 1``
+        collapses every plate's objects onto one bed. That is a property of
+        the flag, not of the cross-class detour that first needed it — so a
+        user ticking auto-arrange over "all plates" on a SAME-class source
+        must take the same loop. Keying the loop on the cross-class decision
+        alone would send one call and silently return a one-plate result.
+        """
+        from backend.app.models.archive import PrintArchive
+
+        tmp_path = slice_test_setup["tmp_path"]
+        monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
+
+        src_dir = tmp_path / "archives" / "src_same_class"
+        src_dir.mkdir(parents=True, exist_ok=True)
+        src_3mf = src_dir / "tray.3mf"
+        src_3mf.write_bytes(self._make_multi_plate_x1c_source(plate_count=2))
+        printer = await printer_factory()
+        source = await archive_factory(
+            printer.id,
+            filename="tray.3mf",
+            file_path=str(src_3mf.relative_to(tmp_path)),
+            sliced_for_model="X1C",
+            with_run=False,
+        )
+
+        # X1C target: same nozzle class as the X1C source, so #1493's
+        # detector stays off and only the user's flag is in play.
+        x1c = LocalPreset(
+            name="# Bambu Lab X1 Carbon 0.4 nozzle",
+            preset_type="printer",
+            source="orcaslicer",
+            setting=json.dumps({"name": "Bambu Lab X1 Carbon 0.4 nozzle", "printer_model": "Bambu Lab X1 Carbon"}),
+        )
+        db_session.add(x1c)
+        await db_session.commit()
+        await db_session.refresh(x1c)
+
+        captured_requests: list[dict] = []
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            if not _is_slice_post(request):
+                return httpx.Response(404)
+            body = request.content
+            plate = None
+            marker = b'name="plate"\r\n\r\n'
+            idx = body.find(marker)
+            if idx != -1:
+                start = idx + len(marker)
+                end = body.find(b"\r\n", start)
+                try:
+                    plate = int(body[start:end].decode("utf-8"))
+                except (UnicodeDecodeError, ValueError):
+                    plate = None
+            captured_requests.append(
+                {
+                    "plate": plate,
+                    "arrange": b'name="arrange"' in body,
+                    "orient": b'name="orient"' in body,
+                }
+            )
+            return httpx.Response(
+                status_code=200,
+                content=self._make_single_plate_sliced_output(plate or 1),
+                headers={
+                    "x-print-time-seconds": "300",
+                    "x-filament-used-g": "2.0",
+                    "x-filament-used-mm": "800.0",
+                },
+            )
+
+        _install_mock_sidecar(handler)
+
+        resp = await async_client.post(
+            f"/api/v1/archives/{source.id}/slice",
+            json={
+                "printer_preset": {"source": "local", "id": str(x1c.id)},
+                "process_preset": {"source": "local", "id": str(slice_test_setup["process_id"])},
+                "filament_presets": [{"source": "local", "id": str(slice_test_setup["filament_id"])}],
+                "plate": 0,
+                "auto_arrange": True,
+                "auto_orient": True,
+            },
+        )
+        assert resp.status_code == 202, resp.text
+        final = await _wait_for_job(async_client, resp.json()["job_id"], timeout=15.0)
+        assert final["status"] == "completed", final
+
+        assert [c["plate"] for c in captured_requests] == [1, 2]
+        assert all(c["arrange"] for c in captured_requests)
+        # Orient rides along on every sub-slice too — it is per-object, so
+        # dropping it on the loop path would quietly ignore the user's tick.
+        assert all(c["orient"] for c in captured_requests)
+
+        new_archive = await db_session.get(PrintArchive, final["result"]["archive_id"])
+        with zipfile.ZipFile(tmp_path / new_archive.file_path, "r") as zf:
+            entries = set(zf.namelist())
+        assert "Metadata/plate_1.gcode" in entries
+        assert "Metadata/plate_2.gcode" in entries
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_embedded_settings_slice_all_with_arrange_still_loops(
+        self, async_client: AsyncClient, db_session, slice_test_setup, printer_factory, archive_factory, monkeypatch
+    ):
+        """ "Slice as designed" must not skip the loop. The project-wide
+        collapse comes from ``--arrange``; where the print config came from
+        has no bearing on it. Taking the single-call embedded branch here
+        would return one consolidated plate for a job the user asked to
+        slice as N — and the per-plate calls must still omit the profile
+        triplet, or "as designed" would silently stop meaning that.
+        """
+        from backend.app.models.archive import PrintArchive
+
+        tmp_path = slice_test_setup["tmp_path"]
+        monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
+
+        src_dir = tmp_path / "archives" / "src_embedded"
+        src_dir.mkdir(parents=True, exist_ok=True)
+        src_3mf = src_dir / "kit.3mf"
+        src_3mf.write_bytes(self._make_multi_plate_x1c_source(plate_count=2))
+        printer = await printer_factory()
+        source = await archive_factory(
+            printer.id,
+            filename="kit.3mf",
+            file_path=str(src_3mf.relative_to(tmp_path)),
+            sliced_for_model="X1C",
+            with_run=False,
+        )
+
+        x1c = LocalPreset(
+            name="# Bambu Lab X1 Carbon 0.4 nozzle",
+            preset_type="printer",
+            source="orcaslicer",
+            setting=json.dumps({"name": "Bambu Lab X1 Carbon 0.4 nozzle", "printer_model": "Bambu Lab X1 Carbon"}),
+        )
+        db_session.add(x1c)
+        await db_session.commit()
+        await db_session.refresh(x1c)
+
+        captured_requests: list[dict] = []
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            if not _is_slice_post(request):
+                return httpx.Response(404)
+            body = request.content
+            plate = None
+            marker = b'name="plate"\r\n\r\n'
+            idx = body.find(marker)
+            if idx != -1:
+                start = idx + len(marker)
+                end = body.find(b"\r\n", start)
+                try:
+                    plate = int(body[start:end].decode("utf-8"))
+                except (UnicodeDecodeError, ValueError):
+                    plate = None
+            captured_requests.append(
+                {
+                    "plate": plate,
+                    "arrange": b'name="arrange"' in body,
+                    "has_profiles": b'name="printerProfile"' in body,
+                }
+            )
+            return httpx.Response(
+                status_code=200,
+                content=self._make_single_plate_sliced_output(plate or 1),
+                headers={
+                    "x-print-time-seconds": "300",
+                    "x-filament-used-g": "2.0",
+                    "x-filament-used-mm": "800.0",
+                },
+            )
+
+        _install_mock_sidecar(handler)
+
+        resp = await async_client.post(
+            f"/api/v1/archives/{source.id}/slice",
+            json={
+                "printer_preset": {"source": "local", "id": str(x1c.id)},
+                "process_preset": {"source": "local", "id": str(slice_test_setup["process_id"])},
+                "filament_presets": [{"source": "local", "id": str(slice_test_setup["filament_id"])}],
+                "plate": 0,
+                "use_embedded_settings": True,
+                "auto_arrange": True,
+            },
+        )
+        assert resp.status_code == 202, resp.text
+        final = await _wait_for_job(async_client, resp.json()["job_id"], timeout=15.0)
+        assert final["status"] == "completed", final
+
+        assert [c["plate"] for c in captured_requests] == [1, 2]
+        assert all(c["arrange"] for c in captured_requests)
+        # No --load-settings on any sub-call: the file's own settings drive
+        # each plate, which is what "slice as designed" promises.
+        assert not any(c["has_profiles"] for c in captured_requests)
+        assert final["result"]["used_embedded_settings"] is True
+
+        new_archive = await db_session.get(PrintArchive, final["result"]["archive_id"])
+        with zipfile.ZipFile(tmp_path / new_archive.file_path, "r") as zf:
+            entries = set(zf.namelist())
+        assert "Metadata/plate_1.gcode" in entries
+        assert "Metadata/plate_2.gcode" in entries
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_cross_class_arrange_survives_user_leaving_the_box_unticked(
+        self, async_client: AsyncClient, db_session, slice_test_setup, printer_factory, archive_factory, monkeypatch
+    ):
+        """The user's per-slice choice is a union with #1493's decision, not
+        a replacement for it. Arrange is what keeps a class-crossing slice
+        from landing in the target's dead zone or segfaulting ZFiller — so
+        the default-false ``auto_arrange`` must not be able to turn it off.
+        """
+        tmp_path = slice_test_setup["tmp_path"]
+        monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
+
+        src_dir = tmp_path / "archives" / "src_cross_single"
+        src_dir.mkdir(parents=True, exist_ok=True)
+        src_3mf = src_dir / "clip.3mf"
+        src_3mf.write_bytes(self._make_multi_plate_x1c_source(plate_count=1))
+        printer = await printer_factory()
+        source = await archive_factory(
+            printer.id,
+            filename="clip.3mf",
+            file_path=str(src_3mf.relative_to(tmp_path)),
+            sliced_for_model="X1C",
+            with_run=False,
+        )
+
+        h2d = LocalPreset(
+            name="# Bambu Lab H2D 0.4 nozzle",
+            preset_type="printer",
+            source="orcaslicer",
+            setting=json.dumps({"name": "Bambu Lab H2D 0.4 nozzle", "printer_model": "Bambu Lab H2D"}),
+        )
+        db_session.add(h2d)
+        await db_session.commit()
+        await db_session.refresh(h2d)
+
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            if not _is_slice_post(request):
+                return httpx.Response(404)
+            captured["body"] = bytes(request.content)
+            return httpx.Response(
+                status_code=200,
+                content=self._make_single_plate_sliced_output(1),
+                headers={
+                    "x-print-time-seconds": "300",
+                    "x-filament-used-g": "2.0",
+                    "x-filament-used-mm": "800.0",
+                },
+            )
+
+        _install_mock_sidecar(handler)
+
+        resp = await async_client.post(
+            f"/api/v1/archives/{source.id}/slice",
+            json={
+                "printer_preset": {"source": "local", "id": str(h2d.id)},
+                "process_preset": {"source": "local", "id": str(slice_test_setup["process_id"])},
+                "filament_presets": [{"source": "local", "id": str(slice_test_setup["filament_id"])}],
+                "plate": 1,
+                "auto_arrange": False,
+            },
+        )
+        assert resp.status_code == 202, resp.text
+        final = await _wait_for_job(async_client, resp.json()["job_id"], timeout=15.0)
+        assert final["status"] == "completed", final
+
+        assert b'name="arrange"' in captured["body"], "cross-class arrange must survive an explicit auto_arrange=false"
+
 
 class TestSliceArchiveResliceModel:
     """Re-slicing an archive for a different printer must stamp the new

+ 109 - 0
backend/tests/unit/services/test_slicer_api.py

@@ -309,6 +309,115 @@ class TestSliceWithProfiles:
 
         assert b'name="arrange"' not in captured["body"]
 
+    @pytest.mark.asyncio
+    async def test_orient_true_emits_form_field(self):
+        """#2548: user-requested auto-orient reaches the sidecar as its own
+        form field, which it turns into ``--orient 1``."""
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            captured["body"] = request.content
+            return httpx.Response(
+                status_code=200,
+                content=b"3MF",
+                headers={"x-print-time-seconds": "0", "x-filament-used-g": "0", "x-filament-used-mm": "0"},
+            )
+
+        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
+        await service.slice_with_profiles(
+            model_bytes=b"x",
+            model_filename="Cube.3mf",
+            printer_profile_json="{}",
+            process_profile_json="{}",
+            filament_profile_jsons=["{}"],
+            orient=True,
+        )
+
+        assert b'name="orient"' in captured["body"]
+
+    @pytest.mark.asyncio
+    async def test_orient_false_omits_form_field(self):
+        """An off flag must be expressed by ABSENCE, never by sending
+        "false". The sidecar branches on ``settings.orient !== undefined``
+        and multipart fields arrive as strings — and ``"false"`` is truthy
+        in JavaScript, so sending it would switch auto-orient ON for every
+        user who left the box unticked."""
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            captured["body"] = request.content
+            return httpx.Response(
+                status_code=200,
+                content=b"3MF",
+                headers={"x-print-time-seconds": "0", "x-filament-used-g": "0", "x-filament-used-mm": "0"},
+            )
+
+        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
+        await service.slice_with_profiles(
+            model_bytes=b"x",
+            model_filename="Cube.3mf",
+            printer_profile_json="{}",
+            process_profile_json="{}",
+            filament_profile_jsons=["{}"],
+            orient=False,
+        )
+
+        body = captured["body"]
+        assert b'name="orient"' not in body
+        assert b"false" not in body
+
+    @pytest.mark.asyncio
+    async def test_profileless_slice_forwards_both_layout_flags(self):
+        """The embedded-settings path and the segfault fallback both run
+        through ``slice_without_profiles``. Arrange / orient are CLI actions
+        on the geometry rather than profile values, so a user's per-slice
+        choice has to survive those routes too (#2548) — before this they
+        could not be expressed there at all."""
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            captured["body"] = request.content
+            return httpx.Response(
+                status_code=200,
+                content=b"3MF",
+                headers={"x-print-time-seconds": "0", "x-filament-used-g": "0", "x-filament-used-mm": "0"},
+            )
+
+        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
+        await service.slice_without_profiles(
+            model_bytes=b"x",
+            model_filename="Cube.3mf",
+            arrange=True,
+            orient=True,
+        )
+
+        body = captured["body"]
+        assert b'name="arrange"' in body
+        assert b'name="orient"' in body
+
+    @pytest.mark.asyncio
+    async def test_profileless_slice_defaults_omit_layout_flags(self):
+        """The filament-discovery preview also uses this method and passes
+        neither flag — it must keep sending the pre-#2548 payload, since
+        rearranging objects would not change which slots a plate consumes
+        but would burn the arrange pass on every preview."""
+        captured: dict = {}
+
+        def handler(request: httpx.Request) -> httpx.Response:
+            captured["body"] = request.content
+            return httpx.Response(
+                status_code=200,
+                content=b"3MF",
+                headers={"x-print-time-seconds": "0", "x-filament-used-g": "0", "x-filament-used-mm": "0"},
+            )
+
+        service = SlicerApiService("http://sidecar:3000", client=_mock_client(handler))
+        await service.slice_without_profiles(model_bytes=b"x", model_filename="Cube.3mf")
+
+        body = captured["body"]
+        assert b'name="arrange"' not in body
+        assert b'name="orient"' not in body
+
     @pytest.mark.asyncio
     async def test_multi_filament_sends_one_part_per_profile(self):
         # Multi-color slicing requires N filament profiles, in plate-slot

+ 26 - 0
backend/tests/unit/test_slice_request_schema.py

@@ -152,3 +152,29 @@ class TestPresetsRequired:
     def test_empty_request_rejected(self):
         with pytest.raises(ValidationError):
             SliceRequest()
+
+
+class TestLayoutFlags:
+    """#2548: auto-orient / auto-arrange are per-slice options on the
+    request, not stored settings. Both must default to off — they rewrite
+    the object placement the file came with, which is never something to do
+    to a user who did not ask for it."""
+
+    def test_both_default_to_off(self):
+        req = SliceRequest(
+            printer_preset=PresetRef(source="local", id="1"),
+            process_preset=PresetRef(source="local", id="2"),
+            filament_preset=PresetRef(source="local", id="3"),
+        )
+        assert req.auto_orient is False
+        assert req.auto_arrange is False
+
+    def test_flags_are_independent(self):
+        req = SliceRequest(
+            printer_preset=PresetRef(source="local", id="1"),
+            process_preset=PresetRef(source="local", id="2"),
+            filament_preset=PresetRef(source="local", id="3"),
+            auto_orient=True,
+        )
+        assert req.auto_orient is True
+        assert req.auto_arrange is False

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

@@ -523,6 +523,57 @@ describe('SliceModal', () => {
     });
   });
 
+  it('sends the layout flags only for the boxes the user ticked (#2548)', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
+      onClose: vi.fn(),
+    });
+
+    await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
+
+    const user = userEvent.setup();
+    await user.click(screen.getByRole('checkbox', { name: /Auto-orient objects/ }));
+    await user.click(screen.getByRole('button', { name: /^Slice$/ }));
+
+    await waitFor(() => {
+      const [, body] = vi.mocked(mockApi.sliceLibraryFile).mock.calls[0];
+      expect(body).toHaveProperty('auto_orient', true);
+      // The untouched box is omitted, not sent as false. The sidecar reads
+      // any present value as truthy, so a literal false would arrange every
+      // slice — the flag has to travel by absence.
+      expect(body).not.toHaveProperty('auto_arrange');
+    });
+  });
+
+  it('omits both layout flags when neither box is ticked (#2548)', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' },
+      onClose: vi.fn(),
+    });
+
+    await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
+
+    await userEvent.setup().click(screen.getByRole('button', { name: /^Slice$/ }));
+
+    await waitFor(() => {
+      const [, body] = vi.mocked(mockApi.sliceLibraryFile).mock.calls[0];
+      expect(body).not.toHaveProperty('auto_orient');
+      expect(body).not.toHaveProperty('auto_arrange');
+    });
+  });
+
   it('lets the user override the default and pick a Standard preset', async () => {
     const onClose = vi.fn();
     mockApi.sliceLibraryFile.mockResolvedValue({

+ 6 - 0
frontend/src/api/client.ts

@@ -1604,6 +1604,12 @@ export interface SliceRequest {
   // instead of the picked profile triplet. The preset refs above are still
   // required by the backend validator but go unused on this path.
   use_embedded_settings?: boolean;
+  // Layout passes the slicer runs before slicing (#2548), both off by
+  // default because they move or rotate the objects the user laid out.
+  // Unlike the fields above these are CLI actions rather than profile
+  // values, so they apply on the embedded-settings path too.
+  auto_orient?: boolean;
+  auto_arrange?: boolean;
 }
 
 // GET /api/v1/slicer/presets — unified listing across cloud / local / standard.

+ 49 - 0
frontend/src/components/SliceModal.tsx

@@ -237,6 +237,15 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // see canUseEmbedded below.
   const [useEmbedded, setUseEmbedded] = useState(false);
 
+  // Auto-orient / auto-arrange (#2548) — the GUI's two layout buttons,
+  // forwarded as the slicer's --orient / --arrange CLI actions. Per-slice
+  // and off by default: both rewrite the object placement the file came
+  // with, so they are something the user asks for, never a default. Kept
+  // enabled in embedded mode, unlike the process-level options around
+  // them — these act on the geometry, whichever config drives the slice.
+  const [autoOrient, setAutoOrient] = useState(false);
+  const [autoArrange, setAutoArrange] = 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
@@ -544,6 +553,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
       // which the embedded-settings path never sends — so they are mutually
       // exclusive by construction (#2622).
       ...(!useEmbedded && designKeys.size > 0 ? { design_overrides: [...designKeys] } : {}),
+      // Sent only when on. The backend defaults both to false, so omitting
+      // them keeps the request identical to what older clients send.
+      ...(autoOrient ? { auto_orient: true } : {}),
+      ...(autoArrange ? { auto_arrange: true } : {}),
     };
   }
 
@@ -888,6 +901,42 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                 onChange={setBedType}
                 disabled={isEnqueuing || useEmbedded}
               />
+              {/* Layout passes (#2548) — the GUI's "Auto orient" / "Auto
+                  arrange". Not disabled in embedded mode: these are CLI
+                  actions on the geometry, so they work regardless of where
+                  the print config comes from. */}
+              <div className="flex flex-col gap-2">
+                <label className="flex items-start gap-2 text-sm text-bambu-gray cursor-pointer select-none">
+                  <input
+                    type="checkbox"
+                    checked={autoOrient}
+                    onChange={(e) => setAutoOrient(e.target.checked)}
+                    disabled={isEnqueuing}
+                    className="mt-0.5 cursor-pointer"
+                  />
+                  <span>
+                    {t('slice.autoOrient')}
+                    <span className="block text-xs text-bambu-gray/70">
+                      {t('slice.autoOrientHint')}
+                    </span>
+                  </span>
+                </label>
+                <label className="flex items-start gap-2 text-sm text-bambu-gray cursor-pointer select-none">
+                  <input
+                    type="checkbox"
+                    checked={autoArrange}
+                    onChange={(e) => setAutoArrange(e.target.checked)}
+                    disabled={isEnqueuing}
+                    className="mt-0.5 cursor-pointer"
+                  />
+                  <span>
+                    {t('slice.autoArrange')}
+                    <span className="block text-xs text-bambu-gray/70">
+                      {t('slice.autoArrangeHint')}
+                    </span>
+                  </span>
+                </label>
+              </div>
               {/* Filament reqs may need a server-side preview-slice for
                   unsliced project files (single-pass, then cached). Show a
                   scoped spinner so the user sees the printer/process

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

@@ -4098,6 +4098,10 @@ 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.',
+    autoOrient: 'Objekte automatisch ausrichten',
+    autoOrientHint: 'Der Slicer dreht jedes Objekt zuerst auf die am besten druckbare Seite. Überschreibt die Ausrichtung aus der Datei.',
+    autoArrange: 'Automatisch auf dem Druckbett anordnen',
+    autoArrangeHint: 'Der Slicer verteilt die Objekte so, dass sie sich nicht mehr überlappen. Ersetzt die Anordnung aus der Datei.',
     designSettings: 'Einstellungen des Erstellers behalten',
     designSettingsHint: 'Diese Datei ändert {{count}} Druckeinstellung(en) gegenüber dem Standardprofil.',
     designSettingsSelected: '{{selected}} von {{total}} ausgewählt',

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

@@ -4132,6 +4132,10 @@ 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.",
+    autoOrient: 'Auto-orient objects',
+    autoOrientHint: 'Let the slicer turn each object onto its best printing side first. Overrides the way the model was laid down in the file.',
+    autoArrange: 'Auto-arrange on the plate',
+    autoArrangeHint: 'Let the slicer position the objects so they no longer overlap. Replaces the layout the file came with.',
     designSettings: 'Keep the designer\'s settings',
     designSettingsHint: 'This file changes {{count}} print setting(s) from the stock profile.',
     designSettingsSelected: '{{selected}} of {{total}} selected',

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

@@ -4101,6 +4101,10 @@ 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.',
+    autoOrient: 'Orientar los objetos automáticamente',
+    autoOrientHint: 'El laminador gira cada objeto hacia su mejor cara de impresión antes de laminar. Sustituye la orientación del archivo.',
+    autoArrange: 'Organizar automáticamente en la base',
+    autoArrangeHint: 'El laminador coloca los objetos para que dejen de solaparse. Sustituye la disposición 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',

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

@@ -4087,6 +4087,10 @@ 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.",
+    autoOrient: 'Orienter les objets automatiquement',
+    autoOrientHint: "Le trancheur fait pivoter chaque objet sur sa meilleure face d'impression avant de trancher. Remplace l'orientation enregistrée dans le fichier.",
+    autoArrange: 'Disposer automatiquement sur le plateau',
+    autoArrangeHint: "Le trancheur place les objets pour qu'ils ne se chevauchent plus. Remplace la disposition 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',

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

@@ -4086,6 +4086,10 @@ 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.',
+    autoOrient: 'Orienta automaticamente gli oggetti',
+    autoOrientHint: "Lo slicer ruota ogni oggetto sul lato che si stampa meglio prima di affettare. Sostituisce l'orientamento salvato nel file.",
+    autoArrange: 'Disponi automaticamente sul piatto',
+    autoArrangeHint: 'Lo slicer dispone gli oggetti in modo che non si sovrappongano più. Sostituisce la disposizione 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',

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

@@ -4098,6 +4098,10 @@ export default {
     allPresetsRequired: 'すべてのプリセットを選択する必要があります',
     useEmbedded: 'ファイルに埋め込まれた設定を使用',
     useEmbeddedHint: '上のプロファイルではなく、設計者が設定したとおり(ウォール、インフィル、フィラメント)にスライスします。お使いのプリンターがファイルと一致するため利用できます。',
+    autoOrient: 'オブジェクトの向きを自動で調整',
+    autoOrientHint: 'スライスする前に、各オブジェクトを印刷に適した面へ自動で回転させます。ファイルに保存された向きは上書きされます。',
+    autoArrange: 'プレート上に自動配置',
+    autoArrangeHint: 'オブジェクトが重ならないようにスライサーが並べ直します。ファイルの配置は置き換えられます。',
     designSettings: '設計者の設定を保持',
     designSettingsHint: 'このファイルは標準プロファイルから {{count}} 個の印刷設定を変更しています。',
     designSettingsSelected: '{{total}} 個中 {{selected}} 個を選択',

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

@@ -3887,6 +3887,10 @@ export default {
     allPresetsRequired: '모든 프리셋을 선택해야 합니다',
     useEmbedded: '파일에 포함된 설정 사용',
     useEmbeddedHint: '위 프로필 대신 디자이너가 설정한 대로(벽, 내부 채움, 필라멘트) 슬라이싱합니다. 프린터가 파일과 일치하여 사용할 수 있습니다.',
+    autoOrient: '개체 방향 자동 조정',
+    autoOrientHint: '슬라이싱하기 전에 각 개체를 출력하기 좋은 면으로 회전시킵니다. 파일에 저장된 방향을 덮어씁니다.',
+    autoArrange: '플레이트에 자동 배치',
+    autoArrangeHint: '개체가 겹치지 않도록 슬라이서가 다시 배치합니다. 파일의 배치를 대체합니다.',
     designSettings: '디자이너 설정 유지',
     designSettingsHint: '이 파일은 기본 프로파일에서 {{count}}개의 출력 설정을 변경합니다.',
     designSettingsSelected: '{{total}}개 중 {{selected}}개 선택됨',

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

@@ -4086,6 +4086,10 @@ 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.',
+    autoOrient: 'Orientar os objetos automaticamente',
+    autoOrientHint: 'O fatiador gira cada objeto para o lado que imprime melhor antes de fatiar. Substitui a orientação salva no arquivo.',
+    autoArrange: 'Organizar automaticamente na mesa',
+    autoArrangeHint: 'O fatiador posiciona os objetos para que não se sobreponham. Substitui a disposição 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',

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

@@ -3883,6 +3883,10 @@ export default {
     allPresetsRequired: "Необходимо выбрать все профили",
     useEmbedded: "Использовать встроенные настройки файла",
     useEmbeddedHint: "Нарезать так, как задумал автор модели (стенки, заполнение, филамент), вместо профилей выше. Доступно, потому что ваш принтер совпадает с указанным в файле.",
+    autoOrient: 'Автоматически ориентировать объекты',
+    autoOrientHint: 'Слайсер повернёт каждый объект на сторону, которая печатается лучше всего. Ориентация из файла будет заменена.',
+    autoArrange: 'Автоматически разместить на столе',
+    autoArrangeHint: 'Слайсер расставит объекты так, чтобы они не перекрывались. Расположение из файла будет заменено.',
     designSettings: "Сохранить настройки автора",
     designSettingsHint: "Этот файл меняет {{count}} настроек печати по сравнению со стандартным профилем.",
     designSettingsSelected: "Выбрано {{selected}} из {{total}}",

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

@@ -4088,6 +4088,10 @@ 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.',
+    autoOrient: 'Nesneleri otomatik yönlendir',
+    autoOrientHint: 'Dilimleyici, dilimlemeden önce her nesneyi en iyi basılan yüzüne çevirir. Dosyada kayıtlı yönlendirmenin yerini alır.',
+    autoArrange: 'Tablaya otomatik yerleştir',
+    autoArrangeHint: 'Dilimleyici nesneleri üst üste binmeyecek şekilde yerleştirir. Dosyadaki yerleşimin yerini alır.',
     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',

+ 4 - 0
frontend/src/i18n/locales/uk.ts

@@ -4132,6 +4132,10 @@ export default {
     allPresetsRequired: "Потрібно вибрати всі профілі",
     useEmbedded: "Використати вбудовані налаштування файлу",
     useEmbeddedHint: "Нарізати модель із налаштуваннями автора файлу — стінками, заповненням і філаментом — замість профілів вище. Ця можливість доступна, оскільки модель принтера відповідає файлу.",
+    autoOrient: "Автоматично орієнтувати об'єкти",
+    autoOrientHint: "Слайсер поверне кожен об'єкт на бік, який друкується найкраще. Орієнтацію з файлу буде замінено.",
+    autoArrange: 'Автоматично розмістити на столі',
+    autoArrangeHint: "Слайсер розставить об'єкти так, щоб вони не перекривалися. Розташування з файлу буде замінено.",
     designSettings: "Зберегти налаштування автора",
     designSettingsHint: "Цей файл змінює {{count}} налаштувань друку порівняно зі стандартним профілем.",
     designSettingsSelected: "Вибрано {{selected}} із {{total}}",

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

@@ -4086,6 +4086,10 @@ export default {
     allPresetsRequired: '必须选择所有预设',
     useEmbedded: '使用文件的内置设置',
     useEmbeddedHint: '按设计者的设置(壁、填充、耗材)切片,而非上方的配置文件。因您的打印机与文件匹配而可用。',
+    autoOrient: '自动摆正模型',
+    autoOrientHint: '切片前由切片器把每个模型转到最适合打印的一面,会覆盖文件中保存的朝向。',
+    autoArrange: '自动排布在热床上',
+    autoArrangeHint: '由切片器重新摆放模型,使其不再重叠,会替换文件自带的布局。',
     designSettings: '保留设计者的设置',
     designSettingsHint: '此文件相对标准配置修改了 {{count}} 项打印设置。',
     designSettingsSelected: '已选择 {{selected}} / {{total}}',

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

@@ -4086,6 +4086,10 @@ export default {
     allPresetsRequired: '必須選擇所有預設',
     useEmbedded: '使用檔案的內建設定',
     useEmbeddedHint: '依設計者的設定(外牆、填充、耗材)切片,而非上方的設定檔。因您的印表機與檔案相符而可用。',
+    autoOrient: '自動擺正模型',
+    autoOrientHint: '切片前由切片器將每個模型轉到最適合列印的一面,會覆蓋檔案中儲存的朝向。',
+    autoArrange: '自動排列在熱床上',
+    autoArrangeHint: '由切片器重新擺放模型,使其不再重疊,會取代檔案自帶的版面配置。',
     designSettings: '保留設計者的設定',
     designSettingsHint: '此檔案相對標準設定檔修改了 {{count}} 項列印設定。',
     designSettingsSelected: '已選擇 {{selected}} / {{total}}',

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-qwJExXvN.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-Dg-sOUv0.js"></script>
+    <script type="module" crossorigin src="/assets/index-qwJExXvN.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-C_6BSgrK.css">
   </head>
   <body>

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