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

Let one checkbox say where a slice's settings come from (issue #2942)

Two features in the slice dialog read as one. "Use the file's built-in
settings" slices a 3MF the way its designer set it up, ignoring the picked
profiles. The per-option "from file" ticks beside each setting carry the
designer's individual deviations onto the profile you picked, and those
arrived pre-ticked whatever the checkbox said. So a slice run deliberately
without the file's settings still took sixteen values out of it -- the
reporter's log names them, enable_support and support_type among them,
landing on a process preset they had chosen on purpose.

The ticks now follow the checkbox. Off, nothing comes out of the file until
it is asked for by name; on, every setting the file changed shows ticked,
because on that path the file really does drive the whole slice. Taking the
designer's work in bulk is still one click, from a line at the top of the
panel that says how many settings the file changed -- it is the only way
left to reach them without hunting for chips across six pages of 348
options -- and it still leaves the machine-tuned keys and the two that are
the picked preset for a per-key decision.

The panel greys out options the slicer's own rules switch off, and it was
evaluating those rules against what the user had typed alone, falling back
to the compiled-in schema defaults for the rest. A preset with supports on
therefore read as enable_support: false and greyed out the whole Support
page while the slice ran supports. A greyed row greyed its tick too, which
is how the reporter's screenshot shows a support type marked "from file",
applied to the slice, and impossible to clear. The rules now see what the
slice will actually run with: the preset's values, the file's values for
the keys that are on, and anything typed on top. The tick is no longer
gated on those rules at all -- it answers a different question, not whether
an option is in play but where its value comes from.

Underneath both, the support carry-over ran outside the ticks entirely,
lifting four keys out of any 3MF that had supports on with nothing on
screen able to decline. It now stands down for the keys that were offered
and turned down, which the request can say for the first time: an empty
design_overrides list means the caller was shown the file's settings and
took none, where no list at all is a caller that predates the choice. That
distinction is what keeps the carry whole for sources with no deviations to
tick, an OrcaSlicer export among them, rather than trading one silent
default for another.

Worth knowing: a Bambu Studio file with supports enabled no longer switches
supports on for you. Tick Enable support, or the checkbox above the panel.
Measured against the reporter's own sixteen keys, and covered by backend
and frontend tests -- reverting any one of the three changes fails tests.
maziggy 1 неделя назад
Родитель
Сommit
b1f5ec9642

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


+ 55 - 8
backend/app/api/routes/library.py

@@ -65,6 +65,7 @@ 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 (
+    DesignOverride,
     apply_design_overrides,
     extract_design_process_overrides,
     overrides_from_config,
@@ -3632,7 +3633,31 @@ _SOURCE_PROCESS_SUPPORT_KEYS_TO_PRESERVE = (
 )
 
 
-def _patch_process_support_settings(process_json: str, source_3mf_bytes: bytes) -> str:
+def _declined_source_keys(offered: list[DesignOverride], requested: list[str] | None) -> set[str]:
+    """Settings the file offered and the caller left unticked (#2942).
+
+    The slice dialog lists what the designer changed and applies only the keys
+    that are switched on, so the answer to "which of these does this slice
+    want" is already in the request. This reads the other half of it — the
+    ones that were on offer and turned down — which the support carry-over
+    below must not put back.
+
+    ``requested`` of ``None`` is a caller that predates the per-key choice and
+    so cannot have declined anything; an empty list is one that was shown the
+    file's settings and took none. Collapsing those two into "nothing
+    selected" is what made an empty panel indistinguishable from an old
+    client, and only one of them means the user said no.
+    """
+    if requested is None:
+        return set()
+    return {override.key for override in offered} - set(requested)
+
+
+def _patch_process_support_settings(
+    process_json: str,
+    source_3mf_bytes: bytes,
+    declined: set[str] | frozenset[str] = frozenset(),
+) -> str:
     """Overlay the source 3MF's support configuration onto the process JSON.
 
     The carry is deliberately one-way: a source can switch supports *on*,
@@ -3645,6 +3670,13 @@ def _patch_process_support_settings(process_json: str, source_3mf_bytes: bytes)
     with supports *on* is by definition a deliberate user preset, since
     Bambu's shipped ones all ship them off.
 
+    ``declined`` names keys the caller offered the user as the file's own
+    (#2622) and that the user left unticked, which this carry must then not
+    reinstate behind their back (#2942). It is empty for a source that offers
+    nothing — an OrcaSlicer export carries no ``different_settings_to_system``,
+    so there is nothing to tick and #1881's blanket carry still applies — and
+    for a client that predates the per-key ticks.
+
     Only fires on 3MF sources — STL / STEP don't carry `project_settings.
     config`. Silently no-ops when the source doesn't have the config, has
     a malformed one, or when the process JSON isn't parseable — the slice
@@ -3672,7 +3704,11 @@ def _patch_process_support_settings(process_json: str, source_3mf_bytes: bytes)
     if not isinstance(process_cfg, dict):
         return process_json
 
-    carried = {key: src_cfg[key] for key in _SOURCE_PROCESS_SUPPORT_KEYS_TO_PRESERVE if key in src_cfg}
+    carried = {
+        key: src_cfg[key] for key in _SOURCE_PROCESS_SUPPORT_KEYS_TO_PRESERVE if key in src_cfg and key not in declined
+    }
+    if not carried:
+        return process_json
     process_cfg.update(carried)
     # Logged because this is the one layer of the process JSON the user
     # can't see coming: the slice modal shows the picked preset's values,
@@ -3860,18 +3896,29 @@ async def _run_slicer_with_fallback(
         # didn't touch) still drive the slice.
         primary_bytes = _sanitize_project_settings_sentinels(primary_bytes)
 
+        # #2622: the process settings the file's designer moved off the stock
+        # preset. Read once — the support patch below needs to know which of
+        # them the user was shown, and the carry after it needs their values.
+        design_offered = extract_design_process_overrides(primary_bytes)
+
+        declined_from_file = _declined_source_keys(design_offered, request.design_overrides)
+
         # #1881: preserve the source 3MF's support configuration on top of
         # the picked process preset. Bambu's shipped process presets set
         # `enable_support: 0` by default (supports are a per-print, not
         # per-quality, decision); `--load-settings` is authoritative so
         # without patching, the source's `enable_support: 1` + support-slot
         # assignments get discarded and the slice comes out single-material
-        # with a PVA slot loaded but never used.
-        presets["process"] = _patch_process_support_settings(presets["process"], primary_bytes)
+        # with a PVA slot loaded but never used. Bounded by the ticks: this
+        # runs for a source that offers no per-key choice at all, and for the
+        # keys of one that does but whose ticks the user left on.
+        presets["process"] = _patch_process_support_settings(
+            presets["process"], primary_bytes, declined=declined_from_file
+        )
 
-        # #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 /
+        # Carry the designer's 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
@@ -3880,7 +3927,7 @@ async def _run_slicer_with_fallback(
         if request.design_overrides:
             presets["process"] = apply_design_overrides(
                 presets["process"],
-                extract_design_process_overrides(primary_bytes),
+                design_offered,
                 request.design_overrides,
             )
 

+ 5 - 1
backend/app/schemas/slicer.py

@@ -90,7 +90,11 @@ class SliceRequest(BaseModel):
             "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."
+            "else is ignored. An empty list is not the same answer as ``None``: "
+            "it says the caller was shown the file's settings and chose none of "
+            "them, which also holds back the support carry-over (#1881) for the "
+            "support keys the file offered, while ``None`` — a caller that "
+            "predates the per-key choice — leaves that carry-over unconditional."
         ),
     )
     process_overrides: dict[str, Any] | None = Field(

+ 111 - 1
backend/tests/unit/test_slice_process_support_patch.py

@@ -25,7 +25,8 @@ import json
 import logging
 import zipfile
 
-from backend.app.api.routes.library import _patch_process_support_settings
+from backend.app.api.routes.library import _declined_source_keys, _patch_process_support_settings
+from backend.app.services.design_settings import DesignOverride
 
 
 def _make_3mf(project_settings: dict | None) -> bytes:
@@ -209,3 +210,112 @@ class TestPatchProcessSupportSettings:
         source = _make_3mf({"enable_support": "1"})
         not_a_dict = json.dumps(["this", "is", "an", "array"])
         assert _patch_process_support_settings(not_a_dict, source) is not_a_dict
+
+
+class TestDeclinedKeysAreNotReinstated:
+    """What the user unticked stays unticked (#2942).
+
+    The slice dialog offers the file's own settings per key and applies only
+    the ones that are on. This carry ran underneath that, unconditionally, so
+    four support keys came out of the file whatever the ticks said -- the
+    reporter's slice took ``enable_support`` and ``support_type`` from a
+    MakerWorld download onto a process preset they had picked deliberately,
+    with the dialog's "Use the file's built-in settings" switched off and
+    nothing on screen able to stop it.
+    """
+
+    def _source(self) -> bytes:
+        return _make_3mf(
+            {
+                "enable_support": "1",
+                "support_filament": "0",
+                "support_interface_filament": "0",
+                "support_type": "normal(auto)",
+            }
+        )
+
+    def _preset(self) -> str:
+        return json.dumps(
+            {
+                "name": "Pokeball Fast - Buddy",
+                "enable_support": "0",
+                "support_type": "tree(auto)",
+                "layer_height": "0.20",
+            }
+        )
+
+    def test_declining_everything_leaves_the_preset_alone(self):
+        result = json.loads(
+            _patch_process_support_settings(
+                self._preset(),
+                self._source(),
+                declined={"enable_support", "support_filament", "support_interface_filament", "support_type"},
+            )
+        )
+        assert result["enable_support"] == "0"
+        assert result["support_type"] == "tree(auto)"
+        assert result["layer_height"] == "0.20"
+
+    def test_declining_one_key_still_carries_the_others(self):
+        # The ticks are per key, so declining the support type is not
+        # declining supports.
+        result = json.loads(_patch_process_support_settings(self._preset(), self._source(), declined={"support_type"}))
+        assert result["enable_support"] == "1"
+        assert result["support_type"] == "tree(auto)"
+
+    def test_declining_nothing_is_the_behaviour_it_always_had(self):
+        # A source that offers no per-key choice -- an OrcaSlicer export has
+        # no `different_settings_to_system` to tick -- keeps #1881 whole.
+        result = json.loads(_patch_process_support_settings(self._preset(), self._source()))
+        assert result["enable_support"] == "1"
+        assert result["support_type"] == "normal(auto)"
+
+    def test_declining_everything_logs_nothing(self, caplog):
+        # The log line exists to name the layer the user can't see coming.
+        # Nothing was carried, so there is nothing to announce.
+        with caplog.at_level(logging.INFO, logger="backend.app.api.routes.library"):
+            _patch_process_support_settings(
+                self._preset(),
+                self._source(),
+                declined={"enable_support", "support_filament", "support_interface_filament", "support_type"},
+            )
+        assert "Carried support settings" not in caplog.text
+
+    def test_declining_a_key_the_source_never_had_changes_nothing(self):
+        result = json.loads(_patch_process_support_settings(self._preset(), self._source(), declined={"wall_loops"}))
+        assert result["enable_support"] == "1"
+        assert result["support_type"] == "normal(auto)"
+
+
+class TestDeclinedSourceKeys:
+    """Reading "the user said no" out of a slice request (#2942)."""
+
+    @staticmethod
+    def _offered(*keys: str) -> list[DesignOverride]:
+        return [DesignOverride(key=key, value="1", printer_coupled=False) for key in keys]
+
+    def test_no_list_at_all_declines_nothing(self):
+        # A client that predates the per-key ticks -- or any API consumer that
+        # never sends the field -- cannot have turned anything down, so the
+        # support carry-over stays exactly as it was.
+        assert _declined_source_keys(self._offered("enable_support"), None) == set()
+
+    def test_an_empty_list_declines_everything_on_offer(self):
+        # Not the same answer as None: the panel was shown, and nothing in it
+        # was ticked.
+        assert _declined_source_keys(self._offered("enable_support", "wall_loops"), []) == {
+            "enable_support",
+            "wall_loops",
+        }
+
+    def test_a_partial_list_declines_only_the_rest(self):
+        offered = self._offered("enable_support", "support_type", "wall_loops")
+        assert _declined_source_keys(offered, ["wall_loops"]) == {"enable_support", "support_type"}
+
+    def test_a_key_that_was_never_offered_is_not_a_decline(self):
+        # Selecting something the file does not list is already ignored when
+        # the values are applied; it must not turn into a phantom refusal.
+        assert _declined_source_keys(self._offered("wall_loops"), ["wall_loops", "layer_height"]) == set()
+
+    def test_a_file_that_offers_nothing_declines_nothing(self):
+        assert _declined_source_keys([], []) == set()

+ 141 - 25
frontend/src/__tests__/components/SliceModal.test.tsx

@@ -376,7 +376,7 @@ describe('SliceModal', () => {
     return found;
   }
 
-  it("carries the design's printer-independent settings by default (#2622)", async () => {
+  it('carries nothing out of the file until it is asked to (#2942)', async () => {
     mockApi.sliceLibraryFile.mockResolvedValue({
       job_id: 42,
       status: 'pending',
@@ -389,8 +389,9 @@ describe('SliceModal', () => {
       onClose: vi.fn(),
     });
 
-    // Two of three pre-selected: the speed key is machine-coupled and is
-    // offered but never pre-ticked.
+    // Nothing pre-ticked: "Use the file's built-in settings" is off, so the
+    // slice runs on the picked preset and the file's own values wait to be
+    // asked for by name.
     await waitFor(() => expect(screen.getByRole('button', { name: /^Slice$/ })).toBeEnabled());
 
     const user = userEvent.setup();
@@ -398,7 +399,10 @@ describe('SliceModal', () => {
 
     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']);
+    // Empty, not absent: the backend reads the difference. A list that is
+    // there and empty says the user was shown the file's settings and took
+    // none of them, which also stands the support carry-over down (#1881).
+    expect(payload.design_overrides).toEqual([]);
   });
 
   // The file's layer height is the one deviation that must not ride along: it
@@ -412,7 +416,7 @@ describe('SliceModal', () => {
     ],
   };
 
-  it("leaves the file's layer height off by default so the picked preset wins", async () => {
+  it("leaves the file's layer height off even in bulk, so the picked preset wins", async () => {
     mockApi.sliceLibraryFile.mockResolvedValue({
       job_id: 42,
       status: 'pending',
@@ -427,11 +431,14 @@ describe('SliceModal', () => {
 
     await waitFor(() => expect(screen.getByRole('button', { name: /^Slice$/ })).toBeEnabled());
 
-    const user = userEvent.setup();
+    const user = await openDesignSection();
+    await user.click(screen.getByRole('button', { name: /Use the designer's settings/ }));
     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[] };
+    // The bulk action takes the keys that carry across printers. Layer height
+    // *is* the preset that was picked, so it stays a per-key decision.
     expect(payload.design_overrides).toEqual(['wall_loops']);
   });
 
@@ -460,7 +467,7 @@ describe('SliceModal', () => {
 
     await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
     const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as { design_overrides?: string[] };
-    expect([...(payload.design_overrides ?? [])].sort()).toEqual(['layer_height', 'wall_loops']);
+    expect(payload.design_overrides).toEqual(['layer_height']);
   });
 
   it('lists every changed setting with its value and flags the machine-coupled ones (#2622)', async () => {
@@ -473,10 +480,14 @@ describe('SliceModal', () => {
 
     const user = await openDesignSection();
 
-    // Carried keys show the designer's value in the option's own control.
+    // Offered but not taken: the row is flagged and the control still shows
+    // the baseline, until the tick says the file's value should win.
     await user.type(screen.getByPlaceholderText('Search settings'), 'wall loops');
+    await waitFor(() => expect(sourceCheckbox('Wall loops')).toBeInTheDocument());
+    expect(sourceCheckbox('Wall loops').checked).toBe(false);
+    expect(screen.getByLabelText(/^Wall loops/)).not.toHaveValue(5);
+    await user.click(sourceCheckbox('Wall loops'));
     await waitFor(() => expect(screen.getByLabelText(/^Wall loops/)).toHaveValue(5));
-    expect(sourceCheckbox('Wall loops').checked).toBe(true);
 
     await user.clear(screen.getByPlaceholderText('Search settings'));
     await user.type(screen.getByPlaceholderText('Search settings'), 'outer wall speed');
@@ -485,7 +496,7 @@ describe('SliceModal', () => {
     expect(sourceCheckbox('Outer wall').checked).toBe(false);
   });
 
-  it('lets the user opt a machine-coupled setting in and a safe one out (#2622)', async () => {
+  it('lets the user opt a machine-coupled setting in on its own (#2622)', async () => {
     mockApi.sliceLibraryFile.mockResolvedValue({
       job_id: 42,
       status: 'pending',
@@ -504,19 +515,123 @@ describe('SliceModal', () => {
     await waitFor(() => expect(sourceCheckbox('Outer wall')).toBeInTheDocument());
     await user.click(sourceCheckbox('Outer wall'));
 
-    await user.clear(screen.getByPlaceholderText('Search settings'));
-    await user.type(screen.getByPlaceholderText('Search settings'), 'wall loops');
-    await waitFor(() => expect(sourceCheckbox('Wall loops')).toBeInTheDocument());
-    await user.click(sourceCheckbox('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']);
+    // Only the key that was asked for -- the two printer-independent ones are
+    // still on offer and still untouched.
+    expect(payload.design_overrides).toEqual(['outer_wall_speed']);
+  });
+
+  it('omits design_overrides entirely for a file that offered nothing (#2942)', async () => {
+    // The other half of the distinction the backend reads: no list at all
+    // means there was nothing to decide, which leaves #1881's support
+    // carry-over unconditional for sources -- an OrcaSlicer export, say --
+    // that record no deviations to tick in the first place.
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+    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());
+    const user = userEvent.setup();
+    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');
+  });
+
+  // #2942: the per-key ticks answer the same question the toggle above them
+  // does -- where do this slice's settings come from -- so one governs the
+  // other. They used to be pre-ticked whatever it said, which is how a slice
+  // with the toggle deliberately off still took sixteen values from the file.
+  const designedForThisPrinter = {
+    ...designedFor,
+    embedded_printer: 'Bambu Lab X1 Carbon 0.4 nozzle',
+    embedded_process: '0.20mm Standard',
+    // Seam position sits on the panel's opening page at the simple tier, so
+    // its tick can be read without driving a panel the toggle has disabled.
+    design_overrides: [
+      ...designedFor.design_overrides,
+      { key: 'seam_position', value: 'rear', printer_coupled: false, preset_defining: false },
+    ],
+  };
+
+  it('shows every setting as coming from the file while the built-in toggle is on (#2942)', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+    mockApi.getLibraryFilePlates.mockResolvedValue(designedForThisPrinter);
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    const user = userEvent.setup();
+    await user.click(await screen.findByLabelText(/Use the file's built-in settings/));
+    await user.click(await screen.findByRole('button', { name: /Process settings/ }));
+    await screen.findByPlaceholderText('Search settings');
+
+    // A readout, not a control: on this path the file drives the whole slice,
+    // so a tick that said otherwise would be describing the wrong run. The
+    // panel is inactive throughout -- nothing here is sent.
+    await waitFor(() => expect(sourceCheckbox('Seam position').checked).toBe(true));
+    expect(sourceCheckbox('Seam position').disabled).toBe(true);
+
+    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[];
+      use_embedded_settings?: boolean;
+    };
+    expect(payload.use_embedded_settings).toBe(true);
+    expect(payload).not.toHaveProperty('design_overrides');
   });
 
-  it('omits design_overrides entirely when the user unticks everything (#2622)', async () => {
+  it('clears them again when the built-in toggle goes back off (#2942)', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({
+      job_id: 42,
+      status: 'pending',
+      status_url: '/api/v1/slice-jobs/42',
+    });
+    mockApi.getLibraryFilePlates.mockResolvedValue(designedForThisPrinter);
+
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    const user = userEvent.setup();
+    const toggle = await screen.findByLabelText(/Use the file's built-in settings/);
+    await user.click(toggle);
+    await user.click(toggle);
+
+    await openDesignSection();
+    await user.type(screen.getByPlaceholderText('Search settings'), 'wall loops');
+    await waitFor(() => expect(sourceCheckbox('Wall loops').checked).toBe(false));
+
+    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[];
+      use_embedded_settings?: boolean;
+    };
+    expect(payload.design_overrides).toEqual([]);
+    expect(payload).not.toHaveProperty('use_embedded_settings');
+  });
+
+  it("takes the designer's settings in bulk, without the machine-coupled ones (#2942)", async () => {
     mockApi.sliceLibraryFile.mockResolvedValue({
       job_id: 42,
       status: 'pending',
@@ -530,17 +645,18 @@ describe('SliceModal', () => {
     });
 
     const user = await openDesignSection();
-    for (const key of ['Wall loops', 'Sparse infill density']) {
-      await user.clear(screen.getByPlaceholderText('Search settings'));
-      await user.type(screen.getByPlaceholderText('Search settings'), key.toLowerCase());
-      await waitFor(() => expect(sourceCheckbox(key)).toBeInTheDocument());
-      if (sourceCheckbox(key).checked) await user.click(sourceCheckbox(key));
-    }
+    // Without this the file's settings would be reachable only by hunting for
+    // chips across six pages of 348 options.
+    expect(screen.getByText(/The designer changed 3 process settings/)).toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: /Use the designer's settings/ }));
 
     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');
+    const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as { design_overrides?: string[] };
+    expect([...(payload.design_overrides ?? [])].sort()).toEqual([
+      'sparse_infill_density',
+      'wall_loops',
+    ]);
   });
 
   it('hides the section for a file that changes nothing (#2622)', async () => {

+ 106 - 0
frontend/src/__tests__/components/SlicerSettingsPanel.test.tsx

@@ -320,6 +320,112 @@ describe("SlicerSettingsPanel — the source file's own settings", () => {
   });
 });
 
+describe('SlicerSettingsPanel — what the slicer\'s rules are read against (#2942)', () => {
+  /** The designer's own support configuration, as a real file records it. */
+  const supportSource: DesignOverride[] = [
+    { key: 'support_type', value: 'normal(auto)', printer_coupled: false, preset_defining: false },
+    { key: 'enable_support', value: '1', printer_coupled: false, preset_defining: false },
+  ];
+
+  it("honours the picked preset's value, not the compiled-in default", async () => {
+    // The reporter's case: a process preset with supports on, nothing typed
+    // into the panel. The rules used to be evaluated against the typed values
+    // alone, which fell back to the schema's `enable_support: false` and
+    // greyed out the whole Support page while the slice ran supports.
+    const user = userEvent.setup();
+    await renderPanel({}, { presetValues: { enable_support: '1' } });
+    const type = await showOption(user, 'Type', 'support_type');
+    expect(type).toBeEnabled();
+  });
+
+  it('still greys the page out when the preset really has supports off', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { presetValues: { enable_support: '0' } });
+    const type = await showOption(user, 'Type', 'support_type');
+    expect(type).toBeDisabled();
+  });
+
+  it("counts a switched-on source setting as one of the slice's values", async () => {
+    // Supports come from the file rather than the preset here. The rows they
+    // gate are as live as if the preset had asked for them.
+    const user = userEvent.setup();
+    await renderPanel(
+      {},
+      { sourceOverrides: supportSource, initialSelected: ['enable_support'] },
+    );
+    const type = await showOption(user, 'Type', 'support_type');
+    expect(type).toBeEnabled();
+  });
+
+  it('leaves the file\'s tick operable on a row the slicer has greyed out', async () => {
+    // The two answer different questions -- "is this option in play" versus
+    // "where does its value come from" -- and folding them together is what
+    // left a ticked source setting applied to the slice with nothing on
+    // screen able to clear it.
+    const user = userEvent.setup();
+    await renderPanel({}, { sourceOverrides: supportSource, initialSelected: ['support_type'] });
+    const type = await showOption(user, 'Type', 'support_type');
+    expect(type).toBeDisabled();
+
+    const tick = screen.getByRole('checkbox', { name: /Use the source file's value for Type/ });
+    expect(tick).toBeEnabled();
+    expect(tick).toBeChecked();
+    await user.click(tick);
+    expect(tick).not.toBeChecked();
+  });
+});
+
+describe('SlicerSettingsPanel — taking the file\'s settings in bulk (#2942)', () => {
+  const mixed: DesignOverride[] = [
+    { key: 'wall_loops', value: '5', printer_coupled: false, preset_defining: false },
+    { key: 'outer_wall_speed', value: '200', printer_coupled: true, preset_defining: false },
+    { key: 'layer_height', value: '0.2', printer_coupled: false, preset_defining: true },
+  ];
+
+  it('says how many the file changed', async () => {
+    await renderPanel({}, { sourceOverrides: mixed });
+    expect(screen.getByText(/The designer changed 3 process settings/)).toBeInTheDocument();
+  });
+
+  it('says nothing for a file that changed nothing', async () => {
+    await renderPanel({});
+    expect(screen.queryByText(/The designer changed/)).toBeNull();
+  });
+
+  it('ticks the ones that carry across printers, and only those', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { sourceOverrides: mixed });
+    await user.click(screen.getByRole('button', { name: /Use the designer's settings/ }));
+
+    await showOption(user, 'Wall loops', 'wall_loops');
+    expect(
+      screen.getByRole('checkbox', { name: /Use the source file's value for Wall loops/ }),
+    ).toBeChecked();
+    // Tuned for the designer's machine, and the one that *is* the picked
+    // preset: both stay a per-key decision.
+    await showOption(user, 'Outer wall', 'outer_wall_speed');
+    expect(
+      screen.getByRole('checkbox', { name: /Use the source file's value for Outer wall/ }),
+    ).not.toBeChecked();
+    await showOption(user, 'Layer height', 'layer_height');
+    expect(
+      screen.getByRole('checkbox', { name: /Use the source file's value for Layer height/ }),
+    ).not.toBeChecked();
+  });
+
+  it('clears every one of them, machine-coupled included', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { sourceOverrides: mixed, initialSelected: ['wall_loops', 'outer_wall_speed'] });
+    await user.click(screen.getByRole('button', { name: /Clear 2/ }));
+    expect(screen.queryByRole('button', { name: /Clear/ })).toBeNull();
+
+    await showOption(user, 'Wall loops', 'wall_loops');
+    expect(
+      screen.getByRole('checkbox', { name: /Use the source file's value for Wall loops/ }),
+    ).not.toBeChecked();
+  });
+});
+
 describe('SlicerSettingsPanel — filament-slot options', () => {
   const filamentChoices: FilamentChoice[] = [
     { index: 1, label: 'Bambu PLA Basic', color: '#FF0000' },

+ 24 - 12
frontend/src/components/SliceModal.tsx

@@ -491,18 +491,24 @@ 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,
-  // and so do the ones that define the picked preset (layer height, first
-  // layer height): pre-ticking those let a file's 0.2 quietly slice over an
-  // explicitly picked 0.08 preset while the dropdown still read 0.08.
+  // The per-key ticks follow "Use the file's built-in settings" (#2942).
+  // Off, nothing comes out of the file unless the user asks for it by name;
+  // on, the file drives the whole slice, so every setting it changed shows
+  // ticked — a readout of what the toggle means rather than a control, since
+  // the embedded path sends no design_overrides at all.
+  //
+  // They used to arrive pre-ticked whatever that toggle said, which is what
+  // #2942 reported: a slice with it deliberately unticked still took sixteen
+  // values from the file, several of them on rows the slicer had greyed out,
+  // where the tick could not be cleared. One checkbox now answers the
+  // question the whole dialog asks — where do this slice's settings come
+  // from — and the per-key ticks refine that answer instead of contradicting
+  // it. Reaching them in bulk is the panel's own "use the designer's
+  // settings", which still leaves the machine-coupled and preset-defining
+  // keys for an explicit per-key choice.
   useEffect(() => {
-    setDesignKeys(
-      new Set(
-        designOverrides.filter((o) => !o.printer_coupled && !o.preset_defining).map((o) => o.key),
-      ),
-    );
-  }, [designOverrides]);
+    setDesignKeys(useEmbedded ? new Set(designOverrides.map((o) => o.key)) : new Set());
+  }, [designOverrides, useEmbedded]);
 
   // 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
@@ -607,7 +613,13 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
       // 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] } : {}),
+      //
+      // Sent whenever the file offered any, an empty list included: the
+      // backend reads a missing list as a client that predates the per-key
+      // ticks and keeps carrying the file's support settings unconditionally
+      // (#1881), where an empty one says the user was shown them and took
+      // none (#2942).
+      ...(!useEmbedded && designOverrides.length > 0 ? { design_overrides: [...designKeys] } : {}),
       // The user's own edits from the settings panel. Like design_overrides
       // these patch the resolved process JSON, so the embedded-settings path
       // (which sends no process JSON at all) cannot carry them.

+ 95 - 3
frontend/src/components/SlicerSettingsPanel.tsx

@@ -158,9 +158,34 @@ export default function SlicerSettingsPanel({
     };
   }, []);
 
+  // What this slice will actually run with, in the same precedence order the
+  // rows display: the picked preset underneath, the designer's value for each
+  // key that is switched on, and anything typed here on top.
+  const effectiveValues = useMemo(() => {
+    const merged: Record<string, SettingValue> = { ...(presetValues ?? {}) };
+    for (const o of sourceOverrides) {
+      if (sourceSelected?.has(o.key)) merged[o.key] = o.value as SettingValue;
+    }
+    // An emptied field is not a value — leaving it in would read as "" and
+    // send the config reader to the schema default, past the preset.
+    for (const [key, value] of Object.entries(values)) {
+      if (value !== undefined && value !== '') merged[key] = value;
+    }
+    return merged;
+  }, [presetValues, sourceOverrides, sourceSelected, values]);
+
+  // The slicer's own `enable_if` rules, evaluated against that rather than
+  // against `values` alone (#2942). `values` holds only what the user typed
+  // here, and the config reader falls back to the *schema* default for
+  // everything else — so a preset with supports on read as
+  // `enable_support: false` and greyed out the whole Support page, including
+  // rows whose "from file" tick was on and whose value the slice used. A
+  // greyed row used to grey its tick too, which left a setting that came from
+  // the file, that the slice applied, and that nothing on screen could
+  // switch off.
   const off = useMemo(
-    () => (data ? disabledKeys(values, data.schema, data.toggles) : new Set<string>()),
-    [data, values],
+    () => (data ? disabledKeys(effectiveValues, data.schema, data.toggles) : new Set<string>()),
+    [data, effectiveValues],
   );
 
   const sourceByKey = useMemo(
@@ -168,6 +193,21 @@ export default function SlicerSettingsPanel({
     [sourceOverrides],
   );
 
+  // The subset a bulk "use the designer's settings" may switch on: everything
+  // the file changed except the values tuned for the designer's own machine
+  // and the two that *are* the picked preset. Those two classes stay a
+  // per-key decision, which is the classification #2622 made and this does
+  // not widen.
+  const carryableSource = useMemo(
+    () => sourceOverrides.filter((o) => !o.printer_coupled && !o.preset_defining),
+    [sourceOverrides],
+  );
+
+  const selectedSourceCount = useMemo(
+    () => sourceOverrides.filter((o) => sourceSelected?.has(o.key)).length,
+    [sourceOverrides, sourceSelected],
+  );
+
   // Source overrides for keys the vendored schema doesn't cover. They still
   // apply — the backend reads their values from the file — so they get a group
   // of their own rather than being dropped from view.
@@ -317,6 +357,46 @@ export default function SlicerSettingsPanel({
         </p>
       )}
 
+      {/* What the file brings, and the only bulk way to take it. Nothing here
+          is pre-ticked any more (#2942), so without this line the designer's
+          settings would be reachable only by hunting for green chips across
+          six pages of 348 options. "Use them" ticks the keys that carry
+          across printers; the machine-tuned ones and the two that define the
+          picked preset stay off, as they always have. */}
+      {sourceOverrides.length > 0 && onToggleSource && (
+        <div className="flex flex-wrap items-center gap-2 rounded border border-bambu-dark-tertiary px-2 py-1.5 text-[0.7rem] text-bambu-gray">
+          <span className="min-w-0 flex-1">
+            {t(
+              'slicerSettings.fromFileSummary',
+              'The designer changed {{count}} process settings in this file. Only the ones you tick are used.',
+              { count: sourceOverrides.length },
+            )}
+          </span>
+          <button
+            type="button"
+            disabled={disabled}
+            onClick={() => carryableSource.forEach((o) => onToggleSource(o.key, true))}
+            title={t(
+              'slicerSettings.fromFileUseAllHint',
+              "Ticks the settings that carry across printers. The ones tuned for the designer's own printer, and the ones that define the preset you picked, stay off.",
+            )}
+            className="shrink-0 rounded border border-bambu-dark-tertiary px-1.5 py-0.5 hover:text-white disabled:opacity-40"
+          >
+            {t('slicerSettings.fromFileUseAll', "Use the designer's settings")}
+          </button>
+          {selectedSourceCount > 0 && (
+            <button
+              type="button"
+              disabled={disabled}
+              onClick={() => sourceOverrides.forEach((o) => onToggleSource(o.key, false))}
+              className="shrink-0 rounded border border-bambu-dark-tertiary px-1.5 py-0.5 hover:text-white disabled:opacity-40"
+            >
+              {t('slicerSettings.fromFileClear', 'Clear {{count}}', { count: selectedSourceCount })}
+            </button>
+          )}
+        </div>
+      )}
+
       {!query.trim() && (
         <div className="flex flex-wrap gap-1">
           {visiblePages.map((p) => (
@@ -358,6 +438,7 @@ export default function SlicerSettingsPanel({
                       onChange={(v) => setValue(key, v)}
                       disabled={disabled || off.has(key)}
                       disabledBySlicer={off.has(key)}
+                      formDisabled={disabled}
                       source={sourceByKey.get(key)}
                       sourceOn={sourceSelected?.has(key) ?? false}
                       onToggleSource={onToggleSource}
@@ -417,6 +498,16 @@ interface RowProps {
   disabled: boolean;
   /** Greyed because the slicer's own rules turn it off, not because the form is busy. */
   disabledBySlicer: boolean;
+  /**
+   * The panel-wide disabled state, without the slicer's per-option rules.
+   *
+   * Gates the "from file" tick, which answers a different question from the
+   * control beside it: not "is this option in play" but "where does its value
+   * come from". An option the slicer has switched off can still be one the
+   * user wants the file's value for once it comes back into play, and folding
+   * the two together is what made a ticked source setting unclearable (#2942).
+   */
+  formDisabled: boolean;
   /** Set when the source file's designer moved this option off the stock preset. */
   source?: DesignOverride;
   sourceOn?: boolean;
@@ -434,6 +525,7 @@ function OptionRow({
   onChange,
   disabled,
   disabledBySlicer,
+  formDisabled,
   source,
   sourceOn = false,
   onToggleSource,
@@ -509,7 +601,7 @@ function OptionRow({
             <input
               type="checkbox"
               checked={sourceOn}
-              disabled={disabled}
+              disabled={formDisabled}
               onChange={(e) => onToggleSource(optionKey, e.target.checked)}
               aria-label={t('slicerSettings.useFromFile', "Use the source file's value for {{option}}", {
                 option: option.label || optionKey,

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

@@ -4342,6 +4342,10 @@ export default {
       'Die Datei setzt hier {{value}}, das gewählte Preset verwendet {{preset}}. Nur aktivieren, wenn die Datei Vorrang haben soll.',
     useFromFile: 'Wert aus der Quelldatei für {{option}} verwenden',
     otherFromFile: 'Weitere Einstellungen aus dieser Datei',
+    fromFileSummary: 'Der Designer hat in dieser Datei {{count}} Prozesseinstellungen geändert. Verwendet werden nur die, die du ankreuzt.',
+    fromFileUseAll: 'Einstellungen des Designers übernehmen',
+    fromFileUseAllHint: 'Kreuzt die Einstellungen an, die sich auf andere Drucker übertragen lassen. Die auf den Drucker des Designers abgestimmten und die, die dein gewähltes Preset ausmachen, bleiben aus.',
+    fromFileClear: '{{count}} zurücksetzen',
     loading: 'Slicer-Einstellungen werden geladen…',
     mode: {
       simple: 'Einfach',

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

@@ -4377,6 +4377,10 @@ export default {
       'The file sets this to {{value}} where the preset you picked uses {{preset}}. Tick it only if the file should win.',
     useFromFile: "Use the source file's value for {{option}}",
     otherFromFile: 'Other settings from this file',
+    fromFileSummary: 'The designer changed {{count}} process settings in this file. Only the ones you tick are used.',
+    fromFileUseAll: "Use the designer's settings",
+    fromFileUseAllHint: "Ticks the settings that carry across printers. The ones tuned for the designer's own printer, and the ones that define the preset you picked, stay off.",
+    fromFileClear: 'Clear {{count}}',
     loading: 'Loading slicer settings…',
     mode: {
       simple: 'Simple',

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

@@ -4344,6 +4344,10 @@ export default {
       'El archivo lo fija en {{value}} mientras que el perfil elegido usa {{preset}}. Actívalo solo si debe mandar el archivo.',
     useFromFile: 'Usar el valor del archivo de origen para {{option}}',
     otherFromFile: 'Otros ajustes de este archivo',
+    fromFileSummary: 'El diseñador cambió {{count}} ajustes de proceso en este archivo. Solo se usan los que marques.',
+    fromFileUseAll: 'Usar los ajustes del diseñador',
+    fromFileUseAllHint: 'Marca los ajustes que se trasladan a otras impresoras. Los ajustados a la impresora del diseñador, y los que definen el perfil que elegiste, quedan sin marcar.',
+    fromFileClear: 'Borrar {{count}}',
     loading: 'Cargando ajustes del laminador…',
     mode: {
       simple: 'Simple',

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

@@ -4331,6 +4331,10 @@ export default {
       'Le fichier impose {{value}} alors que le profil choisi utilise {{preset}}. À cocher uniquement si le fichier doit primer.',
     useFromFile: 'Utiliser la valeur du fichier source pour {{option}}',
     otherFromFile: 'Autres paramètres de ce fichier',
+    fromFileSummary: 'Le concepteur a modifié {{count}} réglages de process dans ce fichier. Seuls ceux que vous cochez sont utilisés.',
+    fromFileUseAll: 'Utiliser les réglages du concepteur',
+    fromFileUseAllHint: "Coche les réglages transposables d'une imprimante à l'autre. Ceux calés sur l'imprimante du concepteur, et ceux qui définissent le profil choisi, restent décochés.",
+    fromFileClear: 'Effacer {{count}}',
     loading: 'Chargement des paramètres du trancheur…',
     mode: {
       simple: 'Simple',

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

@@ -4330,6 +4330,10 @@ export default {
       'Il file imposta {{value}} mentre il profilo scelto usa {{preset}}. Selezionalo solo se deve prevalere il file.',
     useFromFile: 'Usa il valore del file di origine per {{option}}',
     otherFromFile: 'Altre impostazioni da questo file',
+    fromFileSummary: 'Il progettista ha modificato {{count}} impostazioni di processo in questo file. Vengono usate solo quelle che spunti.',
+    fromFileUseAll: 'Usa le impostazioni del progettista',
+    fromFileUseAllHint: 'Spunta le impostazioni che si trasferiscono ad altre stampanti. Quelle tarate sulla stampante del progettista, e quelle che definiscono il preset scelto, restano disattivate.',
+    fromFileClear: 'Azzera {{count}}',
     loading: 'Caricamento impostazioni dello slicer…',
     mode: {
       simple: 'Semplice',

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

@@ -4342,6 +4342,10 @@ export default {
       'ファイルはこれを {{value}} に設定していますが、選択したプリセットは {{preset}} です。ファイルを優先する場合のみチェックしてください。',
     useFromFile: '{{option}} に元ファイルの値を使用する',
     otherFromFile: 'このファイルのその他の設定',
+    fromFileSummary: 'このファイルではデザイナーが{{count}}件のプロセス設定を変更しています。使われるのはチェックしたものだけです。',
+    fromFileUseAll: 'デザイナーの設定を使う',
+    fromFileUseAllHint: '他のプリンターでも通用する設定にチェックを入れます。デザイナーのプリンター向けに調整された設定と、選んだプリセットを定義する設定はオフのままです。',
+    fromFileClear: '{{count}}件を解除',
     loading: 'スライサー設定を読み込んでいます…',
     mode: {
       simple: 'シンプル',

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

@@ -4133,6 +4133,10 @@ export default {
       '파일은 이 값을 {{value}}(으)로 설정하지만 선택한 프리셋은 {{preset}}을(를) 사용합니다. 파일을 우선할 때만 선택하세요.',
     useFromFile: '{{option}}에 원본 파일의 값 사용',
     otherFromFile: '이 파일의 기타 설정',
+    fromFileSummary: '이 파일에서 디자이너가 프로세스 설정 {{count}}개를 변경했습니다. 체크한 항목만 적용됩니다.',
+    fromFileUseAll: '디자이너 설정 사용',
+    fromFileUseAllHint: '다른 프린터에서도 통하는 설정에 체크합니다. 디자이너의 프린터에 맞춘 설정과 선택한 프리셋을 정의하는 설정은 꺼진 채로 둡니다.',
+    fromFileClear: '{{count}}개 해제',
     loading: '슬라이서 설정을 불러오는 중…',
     mode: {
       simple: '간단',

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

@@ -4330,6 +4330,10 @@ export default {
       'O arquivo define {{value}} enquanto o perfil escolhido usa {{preset}}. Marque apenas se o arquivo deve prevalecer.',
     useFromFile: 'Usar o valor do arquivo de origem para {{option}}',
     otherFromFile: 'Outras configurações deste arquivo',
+    fromFileSummary: 'O designer alterou {{count}} configurações de processo neste arquivo. Só as que você marcar são usadas.',
+    fromFileUseAll: 'Usar as configurações do designer',
+    fromFileUseAllHint: 'Marca as configurações que valem em outras impressoras. As ajustadas para a impressora do designer, e as que definem o preset escolhido, ficam desmarcadas.',
+    fromFileClear: 'Limpar {{count}}',
     loading: 'Carregando configurações do fatiador…',
     mode: {
       simple: 'Simples',

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

@@ -4125,6 +4125,10 @@ export default {
       'В файле задано {{value}}, а выбранный пресет использует {{preset}}. Отмечайте, только если приоритет должен быть у файла.',
     useFromFile: 'Использовать значение из исходного файла для «{{option}}»',
     otherFromFile: 'Другие параметры из этого файла',
+    fromFileSummary: "Дизайнер изменил в этом файле {{count}} параметров процесса. Применяются только отмеченные.",
+    fromFileUseAll: "Использовать настройки дизайнера",
+    fromFileUseAllHint: "Отмечает параметры, которые переносятся на другие принтеры. Подогнанные под принтер дизайнера и задающие выбранный пресет остаются выключенными.",
+    fromFileClear: "Снять {{count}}",
     loading: 'Загрузка настроек слайсера…',
     mode: {
       simple: 'Простой',

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

@@ -4331,6 +4331,10 @@ export default {
       'Dosya bunu {{value}} olarak ayarlıyor, seçtiğiniz profil ise {{preset}} kullanıyor. Yalnızca dosya öncelikli olacaksa işaretleyin.',
     useFromFile: '{{option}} için kaynak dosyadaki değeri kullan',
     otherFromFile: 'Bu dosyadaki diğer ayarlar',
+    fromFileSummary: 'Tasarımcı bu dosyada {{count}} işlem ayarını değiştirmiş. Yalnızca işaretlediklerin kullanılır.',
+    fromFileUseAll: 'Tasarımcının ayarlarını kullan',
+    fromFileUseAllHint: 'Başka yazıcılara taşınabilen ayarları işaretler. Tasarımcının kendi yazıcısına göre ayarlananlar ve seçtiğin ön ayarı tanımlayanlar kapalı kalır.',
+    fromFileClear: '{{count}} ayarı temizle',
     loading: 'Dilimleyici ayarları yükleniyor…',
     mode: {
       simple: 'Basit',

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

@@ -4375,6 +4375,10 @@ export default {
       'У файлі задано {{value}}, а вибраний пресет використовує {{preset}}. Позначайте, лише якщо перевагу має файл.',
     useFromFile: 'Використовувати значення з вихідного файлу для «{{option}}»',
     otherFromFile: 'Інші параметри з цього файлу',
+    fromFileSummary: "Дизайнер змінив у цьому файлі {{count}} параметрів процесу. Застосовуються лише позначені.",
+    fromFileUseAll: "Використати налаштування дизайнера",
+    fromFileUseAllHint: "Позначає параметри, які переносяться на інші принтери. Підлаштовані під принтер дизайнера та ті, що визначають обраний пресет, лишаються вимкненими.",
+    fromFileClear: "Зняти {{count}}",
     loading: 'Завантаження налаштувань слайсера…',
     mode: {
       simple: 'Простий',

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

@@ -4330,6 +4330,10 @@ export default {
       '文件将其设为 {{value}},而所选预设使用 {{preset}}。仅当应以文件为准时才勾选。',
     useFromFile: '对 {{option}} 使用源文件中的值',
     otherFromFile: '此文件中的其他设置',
+    fromFileSummary: '设计者在此文件中改动了 {{count}} 项工艺设置。只有你勾选的才会生效。',
+    fromFileUseAll: '使用设计者的设置',
+    fromFileUseAllHint: '勾选可跨打印机通用的设置。针对设计者自己打印机调校的,以及决定你所选预设的那些,保持不勾选。',
+    fromFileClear: '清除 {{count}} 项',
     loading: '正在加载切片设置…',
     mode: {
       simple: '简单',

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

@@ -4330,6 +4330,10 @@ export default {
       '檔案將其設為 {{value}},而所選預設使用 {{preset}}。僅在應以檔案為準時才勾選。',
     useFromFile: '對 {{option}} 使用來源檔案中的值',
     otherFromFile: '此檔案中的其他設定',
+    fromFileSummary: '設計者在此檔案中改動了 {{count}} 項製程設定。只有你勾選的才會生效。',
+    fromFileUseAll: '使用設計者的設定',
+    fromFileUseAllHint: '勾選可跨印表機通用的設定。針對設計者自己印表機調校的,以及決定你所選預設集的那些,維持不勾選。',
+    fromFileClear: '清除 {{count}} 項',
     loading: '正在載入切片設定…',
     mode: {
       simple: '簡易',

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

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