maziggy 4 주 전
부모
커밋
c384911f7c
33개의 변경된 파일1133개의 추가작업 그리고 241개의 파일을 삭제
  1. 4 1
      .gitignore
  2. 0 0
      CHANGELOG.md
  3. 27 1
      frontend/scripts/generate-slicer-schema.mjs
  4. 221 20
      frontend/src/__tests__/components/SliceModal.test.tsx
  5. 176 3
      frontend/src/__tests__/components/SlicerSettingsPanel.test.tsx
  6. 58 0
      frontend/src/__tests__/utils/slicerPrinterMatch.test.ts
  7. 43 1
      frontend/src/__tests__/utils/slicerToggle.test.ts
  8. 120 99
      frontend/src/components/SliceModal.tsx
  9. 260 28
      frontend/src/components/SlicerSettingsPanel.tsx
  10. 0 0
      frontend/src/data/slicer/process-schema.json
  11. 0 0
      frontend/src/data/slicer/process-toggle-rules.json
  12. 0 0
      frontend/src/data/slicer/process-ui-tree.json
  13. 13 5
      frontend/src/i18n/locales/de.ts
  14. 13 5
      frontend/src/i18n/locales/en.ts
  15. 13 5
      frontend/src/i18n/locales/es.ts
  16. 13 5
      frontend/src/i18n/locales/fr.ts
  17. 13 5
      frontend/src/i18n/locales/it.ts
  18. 13 5
      frontend/src/i18n/locales/ja.ts
  19. 13 5
      frontend/src/i18n/locales/ko.ts
  20. 13 5
      frontend/src/i18n/locales/pt-BR.ts
  21. 13 5
      frontend/src/i18n/locales/ru.ts
  22. 13 5
      frontend/src/i18n/locales/tr.ts
  23. 13 5
      frontend/src/i18n/locales/uk.ts
  24. 13 5
      frontend/src/i18n/locales/zh-CN.ts
  25. 13 5
      frontend/src/i18n/locales/zh-TW.ts
  26. 9 8
      frontend/src/lib/slicerSettings.ts
  27. 3 3
      frontend/src/types/slicerSettings.ts
  28. 36 5
      frontend/src/utils/slicerPrinterMatch.ts
  29. 1 0
      static/assets/index-D4VkH83v.css
  30. 0 0
      static/assets/index-DGpcnZPX.js
  31. 0 1
      static/assets/index-DcBH50JZ.css
  32. 4 4
      static/assets/process-schema-CzfeynAH.js
  33. 2 2
      static/index.html

+ 4 - 1
.gitignore

@@ -60,7 +60,10 @@ firmware/
 # Node modules
 node_modules/
 
-data/
+# Runtime data dir (db, archives, backups). Anchored to the repo root on
+# purpose: a bare `data/` also matches frontend/src/data and
+# backend/app/data, which are source, not runtime state.
+/data/
 
 # Local-dev runtime caches (matplotlib MPLCONFIGDIR lands here when DATA_DIR
 # is unset, so base_dir resolves to the repo root). In Docker this sits

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
CHANGELOG.md


+ 27 - 1
frontend/scripts/generate-slicer-schema.mjs

@@ -80,11 +80,37 @@ for (const k of CONDITION_KEYS) if (schema[k]) referenced.add(k);
 // options, most of it source-location bookkeeping we have no use for.
 const KEEP = ['type', 'mode', 'label', 'tooltip', 'sidetext', 'min', 'max', 'enum_values', 'enum_labels', 'default'];
 
+// The extractor reads defaults and bounds straight out of C++ initialisers, so
+// float literals arrive in source form: `0.` stays "0.", `0.3f` stays "0.3f",
+// `100.%` stays "100.%", and `0.f` even splits into [0, "f"]. Rendering those
+// verbatim put a column of "0." in the Line width group. They are literal
+// artefacts, not values, so they are cleaned here — once, in the data — rather
+// than worked around in every place that displays a default.
+function normaliseLiteral(value) {
+  if (Array.isArray(value)) {
+    // `0.f` split across two entries; the stray "f" is not a value.
+    const cleaned = value.filter((v) => v !== 'f').map(normaliseLiteral);
+    return cleaned.length > 0 ? cleaned : [0];
+  }
+  if (typeof value !== 'string') return value;
+
+  let s = value.trim();
+  s = s.replace(/^(-?[\d.]+)f$/, '$1');   // 0.3f -> 0.3,  0.f -> 0.
+  s = s.replace(/^(-?[\d.]*)\.%$/, '$1%'); // 100.% -> 100%
+  s = s.replace(/^(-?[\d.]*)\.$/, '$1');   // 0. -> 0
+  // A literal that was nothing but a dot carried no digits to keep.
+  if (s === '' || s === '-') return value;
+  return s;
+}
+
 const trimmedSchema = {};
 for (const key of [...referenced].sort()) {
   const opt = schema[key];
   const out = {};
-  for (const f of KEEP) if (opt[f] !== undefined) out[f] = opt[f];
+  for (const f of KEEP) {
+    if (opt[f] === undefined) continue;
+    out[f] = f === 'default' || f === 'min' || f === 'max' ? normaliseLiteral(opt[f]) : opt[f];
+  }
   trimmedSchema[key] = out;
 }
 

+ 221 - 20
frontend/src/__tests__/components/SliceModal.test.tsx

@@ -33,6 +33,7 @@ vi.mock('../../api/client', () => ({
     // Slicer Pipelines (#1425)
     listSlicerPipelines: vi.fn(),
     createSlicerPipeline: vi.fn(),
+    getSlicerPrinterModels: vi.fn(),
   },
 }));
 
@@ -47,6 +48,7 @@ const mockApi = api as unknown as {
   getArchiveFilamentRequirements: ReturnType<typeof vi.fn>;
   listSlicerPipelines: ReturnType<typeof vi.fn>;
   createSlicerPipeline: ReturnType<typeof vi.fn>;
+  getSlicerPrinterModels: ReturnType<typeof vi.fn>;
 };
 
 function makeUnified(overrides: Partial<UnifiedPresetsResponse> = {}): UnifiedPresetsResponse {
@@ -350,12 +352,27 @@ describe('SliceModal', () => {
     ],
   };
 
+  // The designer's settings are shown inside the process-settings panel now,
+  // against the options they belong to, rather than in a list of their own.
+  // The payload contract below is unchanged: their *values* still travel as
+  // design_overrides keys, read from the file by the backend.
   async function openDesignSection() {
     const user = userEvent.setup();
-    await user.click(await screen.findByText(/Keep the designer's settings/));
+    await user.click(await screen.findByRole('button', { name: /Process settings/ }));
+    await screen.findByPlaceholderText('Search settings');
+    // Every designer key must be reachable, including expert-tier ones.
+    await user.click(screen.getByRole('button', { name: 'Expert' }));
     return user;
   }
 
+  /** The panel's per-option "use the file's value" checkbox, by option key. */
+  function sourceCheckbox(key: string): HTMLInputElement {
+    const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
+    const found = boxes.find((b) => (b.getAttribute('aria-label') ?? '').includes(key));
+    if (!found) throw new Error(`no source checkbox for ${key}`);
+    return found;
+  }
+
   it("carries the design's printer-independent settings by default (#2622)", async () => {
     mockApi.sliceLibraryFile.mockResolvedValue({
       job_id: 42,
@@ -369,8 +386,9 @@ describe('SliceModal', () => {
       onClose: vi.fn(),
     });
 
-    // Two of three pre-selected: the speed key is machine-coupled.
-    expect(await screen.findByText('2 of 3 selected')).toBeInTheDocument();
+    // Two of three pre-selected: the speed key is machine-coupled and is
+    // offered but never pre-ticked.
+    await waitFor(() => expect(screen.getByRole('button', { name: /^Slice$/ })).toBeEnabled());
 
     const user = userEvent.setup();
     await user.click(screen.getByRole('button', { name: /^Slice$/ }));
@@ -388,15 +406,18 @@ describe('SliceModal', () => {
       onClose: vi.fn(),
     });
 
-    await openDesignSection();
+    const user = await openDesignSection();
+
+    // Carried keys show the designer's value in the option's own control.
+    await user.type(screen.getByPlaceholderText('Search settings'), 'wall loops');
+    await waitFor(() => expect(screen.getByLabelText(/^Wall loops/)).toHaveValue(5));
+    expect(sourceCheckbox('Wall loops').checked).toBe(true);
 
-    expect(screen.getByText('wall_loops')).toBeInTheDocument();
-    expect(screen.getByText('5')).toBeInTheDocument();
-    expect(screen.getByText('sparse_infill_density')).toBeInTheDocument();
-    expect(screen.getByText('100%')).toBeInTheDocument();
-    // The risky one is listed too — visible, explained, just not pre-ticked.
-    expect(screen.getByText('outer_wall_speed')).toBeInTheDocument();
-    expect(screen.getByText('printer-specific')).toBeInTheDocument();
+    await user.clear(screen.getByPlaceholderText('Search settings'));
+    await user.type(screen.getByPlaceholderText('Search settings'), 'outer wall speed');
+    // The machine-coupled one is present and flagged, just not pre-ticked.
+    await waitFor(() => expect(screen.getAllByText("designer's printer").length).toBeGreaterThan(0));
+    expect(sourceCheckbox('Outer wall').checked).toBe(false);
   });
 
   it('lets the user opt a machine-coupled setting in and a safe one out (#2622)', async () => {
@@ -413,12 +434,15 @@ describe('SliceModal', () => {
     });
 
     const user = await openDesignSection();
-    const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
-    const byKey = (key: string) =>
-      boxes.find((b) => b.closest('label')?.textContent?.includes(key)) as HTMLInputElement;
 
-    await user.click(byKey('outer_wall_speed'));
-    await user.click(byKey('wall_loops'));
+    await user.type(screen.getByPlaceholderText('Search settings'), 'outer wall speed');
+    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$/ }));
 
@@ -441,9 +465,11 @@ describe('SliceModal', () => {
     });
 
     const user = await openDesignSection();
-    const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
-    for (const box of boxes) {
-      if (box.checked) await user.click(box);
+    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));
     }
 
     await user.click(screen.getByRole('button', { name: /^Slice$/ }));
@@ -461,7 +487,10 @@ describe('SliceModal', () => {
     });
 
     await waitFor(() => expect(screen.getByRole('button', { name: /^Slice$/ })).toBeEnabled());
-    expect(screen.queryByText(/Keep the designer's settings/)).toBeNull();
+    // The panel still exists — it is the editor — but nothing is marked as
+    // coming from the file.
+    expect(screen.queryByText('from file')).toBeNull();
+    expect(screen.queryByText("designer's printer")).toBeNull();
   });
 
   it('includes bed_type in the request when the user picks a non-auto plate (#1337)', async () => {
@@ -1523,6 +1552,73 @@ describe('SliceModal', () => {
  * test setup pins matchMedia to `matches: false`, so every other test in this
  * file exercises the narrow single-stack path; these override it.
  */
+describe('SliceModal — process settings in "slice as designed" mode', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockApi.getSlicerPresets.mockResolvedValue(fullThreeTier);
+    mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [] });
+    mockApi.getSlicerPrinterModels.mockResolvedValue({});
+    mockApi.getLibraryFilePlates.mockResolvedValue({
+      file_id: 100,
+      filename: 'Designed.3mf',
+      plates: [],
+      is_multi_plate: false,
+      embedded_printer: 'Bambu Lab X1 Carbon 0.4 nozzle',
+      embedded_process: '0.20mm Standard',
+    });
+    mockApi.getLibraryFileFilamentRequirements.mockResolvedValue({
+      file_id: 100, filename: 'Designed.3mf', plate_id: 1, filaments: [],
+    });
+  });
+
+  it('disables the panel rather than removing it', async () => {
+    const user = userEvent.setup();
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    const toggle = (await screen.findByLabelText(/Use the file's built-in settings/)) as HTMLInputElement;
+    const header = await screen.findByRole('button', { name: /Process settings/ });
+    await user.click(header);
+    const search = await screen.findByPlaceholderText('Search settings');
+    expect(search).toBeEnabled();
+
+    await user.click(toggle);
+
+    // Still on screen — hiding it made the dialog look like it had lost a
+    // feature — but nothing in it can be operated, because nothing in it is
+    // sent on this path.
+    expect(screen.getByPlaceholderText('Search settings')).toBeDisabled();
+    expect(screen.getByRole('button', { name: 'Expert' })).toBeDisabled();
+    expect(screen.getByText(/Not used while/)).toBeInTheDocument();
+    expect(screen.getByText('Inactive')).toBeInTheDocument();
+  });
+
+  it('sends no process overrides once the file drives the slice', async () => {
+    mockApi.sliceLibraryFile.mockResolvedValue({ job_id: 42, status: 'pending', status_url: '/x' });
+    const user = userEvent.setup();
+    renderWithTracker({
+      source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
+      onClose: vi.fn(),
+    });
+
+    // Edit something, then hand the slice over to the file's own settings.
+    await user.click(await screen.findByRole('button', { name: /Process settings/ }));
+    const input = await screen.findByLabelText(/^Layer height/);
+    await user.clear(input);
+    await user.type(input, '0.16');
+
+    await user.click(screen.getByLabelText(/Use the file's built-in settings/));
+    await user.click(screen.getByRole('button', { name: /^Slice$/ }));
+
+    await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
+    const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as Record<string, unknown>;
+    expect(payload.use_embedded_settings).toBe(true);
+    expect(payload).not.toHaveProperty('process_overrides');
+  });
+});
+
 describe('SliceModal — process settings layout', () => {
   const setViewport = (wide: boolean) => {
     Object.defineProperty(window, 'matchMedia', {
@@ -1583,6 +1679,111 @@ describe('SliceModal — process settings layout', () => {
   });
 });
 
+/**
+ * Process and filament lists hold back presets that resolve to a *different*
+ * printer, behind a per-slot "Show all". Two things must never be hidden: a
+ * preset whose compatibility is merely unknown, and whatever is currently
+ * selected.
+ */
+describe('SliceModal — presets filtered by the selected printer', () => {
+  const presets: UnifiedPresetsResponse = {
+    cloud: { printer: [], process: [], filament: [] },
+    orca_cloud: { printer: [], process: [], filament: [] },
+    local: { printer: [], process: [], filament: [] },
+    standard: {
+      printer: [
+        { id: 'Bambu Lab X1 Carbon 0.4 nozzle', name: 'Bambu Lab X1 Carbon 0.4 nozzle', source: 'standard' },
+      ],
+      process: [
+        { id: 'p-x1c', name: '0.20mm Standard @BBL X1C', source: 'standard' },
+        { id: 'p-h2d', name: '0.20mm Standard @BBL H2D', source: 'standard' },
+        { id: 'p-a1m', name: '0.20mm Standard @BBL A1M', source: 'standard' },
+        // No printer tag at all — compatibility is unknown, never hidden.
+        { id: 'p-custom', name: 'My own profile', source: 'standard' },
+      ],
+      filament: [{ id: 'f-x1c', name: 'Bambu PLA Basic @BBL X1C', source: 'standard' }],
+    },
+    cloud_status: 'ok',
+    orca_cloud_status: 'ok',
+  } as UnifiedPresetsResponse;
+
+  const processOptionNames = () =>
+    Array.from(presetSelects()[1].options).map((o) => o.textContent);
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockApi.getSlicerPresets.mockResolvedValue(presets);
+    mockApi.getSlicerPrinterModels.mockResolvedValue({ 'Bambu Lab X1 Carbon': 'X1C' });
+    mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [] });
+    mockApi.getLibraryFilePlates.mockResolvedValue({
+      file_id: 100, filename: 'Cube.stl', plates: [], is_multi_plate: false,
+    });
+    mockApi.getLibraryFileFilamentRequirements.mockResolvedValue({
+      file_id: 100, filename: 'Cube.stl', plate_id: 1, filaments: [],
+    });
+  });
+
+  const open = async () => {
+    renderWithTracker({ source: { kind: 'libraryFile', id: 100, filename: 'Cube.stl' }, onClose: vi.fn() });
+    await waitFor(() => expect(presetSelects().length).toBeGreaterThan(1));
+  };
+
+  it('leaves out presets belonging to another printer', async () => {
+    await open();
+    await waitFor(() => expect(processOptionNames()).toContain('0.20mm Standard @BBL X1C'));
+    expect(processOptionNames()).not.toContain('0.20mm Standard @BBL H2D');
+    expect(processOptionNames()).not.toContain('0.20mm Standard @BBL A1M');
+  });
+
+  it('keeps a preset whose compatibility cannot be determined', async () => {
+    await open();
+    // An untagged preset carries no evidence either way; hiding it would make
+    // a user's own imported profiles vanish.
+    await waitFor(() => expect(processOptionNames()).toContain('My own profile'));
+  });
+
+  it('says how many it held back and reveals them on request', async () => {
+    const user = userEvent.setup();
+    await open();
+
+    const hidden = await screen.findByText('2 hidden');
+    expect(hidden).toBeInTheDocument();
+
+    await user.click(within(hidden.parentElement as HTMLElement).getByRole('button', { name: 'Show all' }));
+    await waitFor(() => expect(processOptionNames()).toContain('0.20mm Standard @BBL H2D'));
+    expect(processOptionNames()).toContain('0.20mm Standard @BBL A1M');
+  });
+
+  it('collapses the list again on Show fewer', async () => {
+    const user = userEvent.setup();
+    await open();
+
+    await user.click((await screen.findAllByRole('button', { name: 'Show all' }))[0]);
+    await waitFor(() => expect(processOptionNames()).toContain('0.20mm Standard @BBL H2D'));
+
+    await user.click(screen.getAllByRole('button', { name: 'Show fewer' })[0]);
+    await waitFor(() => expect(processOptionNames()).not.toContain('0.20mm Standard @BBL H2D'));
+  });
+
+  it('never hides the preset that is currently selected', async () => {
+    const user = userEvent.setup();
+    await open();
+
+    // Reach a cross-printer preset, pick it, then collapse the list again.
+    await user.click((await screen.findAllByRole('button', { name: 'Show all' }))[0]);
+    await waitFor(() => expect(processOptionNames()).toContain('0.20mm Standard @BBL H2D'));
+    await user.selectOptions(presetSelects()[1], 'standard:p-h2d');
+    await user.click(screen.getAllByRole('button', { name: 'Show fewer' })[0]);
+
+    // Dropping it from the options would blank the select and silently discard
+    // a deliberate cross-printer choice.
+    await waitFor(() => expect(processOptionNames()).toContain('0.20mm Standard @BBL H2D'));
+    expect(presetSelects()[1].value).toBe('standard:p-h2d');
+    // The one still-hidden preset is counted; the selected one is not.
+    expect(screen.getByText('1 hidden')).toBeInTheDocument();
+  });
+});
+
 describe('pickFilamentForSlot — printer-compat contract (#1851)', () => {
   // Index that recognises @BBL H2C / @BBL A1 tokens via the canonical
   // PRINTER_MODEL_MAP. Real production data comes through

+ 176 - 3
frontend/src/__tests__/components/SlicerSettingsPanel.test.tsx

@@ -4,8 +4,9 @@ import userEvent from '@testing-library/user-event';
 import { useState } from 'react';
 
 import { render } from '../utils';
-import SlicerSettingsPanel from '../../components/SlicerSettingsPanel';
+import SlicerSettingsPanel, { type FilamentChoice } from '../../components/SlicerSettingsPanel';
 import type { SettingValue } from '../../types/slicerSettings';
+import type { DesignOverride } from '../../types/plates';
 
 /**
  * The panel is a controlled component: it renders from the `values` prop and
@@ -16,11 +17,18 @@ import type { SettingValue } from '../../types/slicerSettings';
 function Harness({
   initial,
   onChange,
+  sourceOverrides,
+  initialSelected,
+  filamentChoices,
 }: {
   initial: Record<string, SettingValue>;
   onChange: (v: Record<string, SettingValue>, s: Record<string, string | string[]>) => void;
+  sourceOverrides?: DesignOverride[];
+  initialSelected?: string[];
+  filamentChoices?: FilamentChoice[];
 }) {
   const [values, setValues] = useState(initial);
+  const [selected, setSelected] = useState(new Set(initialSelected ?? []));
   return (
     <SlicerSettingsPanel
       values={values}
@@ -28,14 +36,32 @@ function Harness({
         setValues(v);
         onChange(v, s);
       }}
+      filamentChoices={filamentChoices}
+      sourceOverrides={sourceOverrides}
+      sourceSelected={selected}
+      onToggleSource={(key, on) =>
+        setSelected((prev) => {
+          const next = new Set(prev);
+          if (on) next.add(key);
+          else next.delete(key);
+          return next;
+        })
+      }
     />
   );
 }
 
 /** Renders the panel and waits for its dynamically imported metadata. */
-async function renderPanel(initial: Record<string, SettingValue> = {}) {
+async function renderPanel(
+  initial: Record<string, SettingValue> = {},
+  extra: {
+    sourceOverrides?: DesignOverride[];
+    initialSelected?: string[];
+    filamentChoices?: FilamentChoice[];
+  } = {},
+) {
   const onChange = vi.fn();
-  render(<Harness initial={initial} onChange={onChange} />);
+  render(<Harness initial={initial} onChange={onChange} {...extra} />);
   await waitFor(() => expect(screen.getByPlaceholderText('Search settings')).toBeInTheDocument());
   return { onChange };
 }
@@ -177,3 +203,150 @@ describe('SlicerSettingsPanel', () => {
     expect(values.wall_loops).toBe(4);
   });
 });
+
+describe('SlicerSettingsPanel — search', () => {
+  it('treats underscores and spaces alike so a key can be typed naturally', async () => {
+    // outer_wall_speed's label is only "Outer wall" — the Speed page supplies
+    // the rest — so the key is the only place the full phrase appears.
+    const user = userEvent.setup();
+    await renderPanel();
+    await user.click(screen.getByRole('button', { name: 'Expert' }));
+    await user.type(screen.getByPlaceholderText('Search settings'), 'outer wall speed');
+    await waitFor(() => expect(screen.getByLabelText(/^Outer wall/)).toBeInTheDocument());
+  });
+
+  it('matches a page or group name, not just option labels', async () => {
+    const user = userEvent.setup();
+    await renderPanel();
+    await user.click(screen.getByRole('button', { name: 'Expert' }));
+    await user.type(screen.getByPlaceholderText('Search settings'), 'ironing');
+    await waitFor(() => expect(screen.getByLabelText(/^Ironing type/)).toBeInTheDocument());
+  });
+});
+
+describe("SlicerSettingsPanel — the source file's own settings", () => {
+  const sourceOverrides: DesignOverride[] = [
+    { key: 'wall_loops', value: '5', printer_coupled: false },
+    { key: 'outer_wall_speed', value: '200', printer_coupled: true },
+    // A key the vendored schema has no entry for. It still applies, so it must
+    // not silently vanish from a panel that claims to show what will be used.
+    { key: 'some_unlisted_key', value: '7', printer_coupled: false },
+  ];
+
+  it("shows the designer's value against the option once switched on", async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { sourceOverrides, initialSelected: ['wall_loops'] });
+    const input = await showOption(user, 'Wall loops', 'wall loops');
+    expect(input).toHaveValue(5);
+    expect(screen.getByText('from file')).toBeInTheDocument();
+  });
+
+  it('falls back to the preset value when it is switched off', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { sourceOverrides, initialSelected: [] });
+    // wall_loops defaults to 2 in the schema.
+    const input = await showOption(user, 'Wall loops', 'wall loops');
+    expect(input).toHaveValue(2);
+  });
+
+  it('flags a machine-coupled setting rather than applying it quietly', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { sourceOverrides, initialSelected: ['wall_loops'] });
+    await user.click(screen.getByRole('button', { name: 'Expert' }));
+    await user.type(screen.getByPlaceholderText('Search settings'), 'outer wall speed');
+    await waitFor(() => expect(screen.getByText("designer's printer")).toBeInTheDocument());
+  });
+
+  it('lists source settings the schema has no entry for', async () => {
+    await renderPanel({}, { sourceOverrides, initialSelected: ['some_unlisted_key'] });
+    await waitFor(() => expect(screen.getByText('Other settings from this file')).toBeInTheDocument());
+    expect(screen.getByText('some_unlisted_key')).toBeInTheDocument();
+    expect(screen.getByText('7')).toBeInTheDocument();
+  });
+
+  it('keeps a typed value ahead of the file\'s', async () => {
+    const user = userEvent.setup();
+    const { onChange } = await renderPanel({}, { sourceOverrides, initialSelected: ['wall_loops'] });
+
+    const input = await showOption(user, 'Wall loops', 'wall loops');
+    expect(input).toHaveValue(5);
+    await user.clear(input);
+    await user.type(input, '3');
+
+    // The typed value is what gets sent; the file's tick is unaffected and the
+    // backend applies it first, so last-write-wins leaves 3 in the process JSON.
+    await waitFor(() => {
+      const [, serialized] = onChange.mock.calls.at(-1)!;
+      expect(serialized.wall_loops).toBe('3');
+    });
+  });
+});
+
+describe('SlicerSettingsPanel — filament-slot options', () => {
+  const filamentChoices: FilamentChoice[] = [
+    { index: 1, label: 'Bambu PLA Basic', color: '#FF0000' },
+    { index: 2, label: 'Bambu Support for PLA', color: '#FFFFFF' },
+  ];
+
+  it("follows the slicer's own gating rather than being live regardless", async () => {
+    // The interface picker sits behind have_support_material, so it greys out
+    // with supports off — becoming a dropdown must not exempt it from the
+    // rules every other option obeys.
+    const user = userEvent.setup();
+    await renderPanel({}, { filamentChoices });
+    const off = await showOption(user, 'Support/raft interface', 'support_interface_filament');
+    expect(off).toBeDisabled();
+  });
+
+  it('is operable once supports are switched on', async () => {
+    const user = userEvent.setup();
+    await renderPanel({ enable_support: true }, { filamentChoices });
+    const on = await showOption(user, 'Support/raft interface', 'support_interface_filament');
+    expect(on).toBeEnabled();
+  });
+
+  it('offers the picked filaments instead of a bare number field', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { filamentChoices });
+    const control = await showOption(user, 'Support/raft base', 'support_filament');
+
+    expect(control.tagName).toBe('SELECT');
+    const labels = Array.from((control as HTMLSelectElement).options).map((o) => o.textContent);
+    expect(labels).toEqual(['Default', '1: Bambu PLA Basic', '2: Bambu Support for PLA']);
+  });
+
+  it("defaults to the slicer's 0, meaning no specific filament", async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { filamentChoices });
+    const control = await showOption(user, 'Support/raft base', 'support_filament');
+    expect(control).toHaveValue('0');
+  });
+
+  it('sends the slot index the slicer expects', async () => {
+    const user = userEvent.setup();
+    const { onChange } = await renderPanel({ enable_support: true }, { filamentChoices });
+    const control = await showOption(user, 'Support/raft interface', 'support_interface_filament');
+
+    await user.selectOptions(control, '2');
+    await waitFor(() => {
+      const [, serialized] = onChange.mock.calls.at(-1)!;
+      expect(serialized.support_interface_filament).toBe('2');
+    });
+  });
+
+  it('stays a plain number field when no filaments have been picked', async () => {
+    // STL sources and the pre-plate-analysis window have no slot list yet;
+    // an empty dropdown would be worse than the number input it replaced.
+    const user = userEvent.setup();
+    await renderPanel({}, { filamentChoices: [] });
+    const control = await showOption(user, 'Support/raft base', 'support_filament');
+    expect(control.tagName).toBe('INPUT');
+  });
+
+  it('leaves unrelated integer options alone', async () => {
+    const user = userEvent.setup();
+    await renderPanel({}, { filamentChoices });
+    const control = await showOption(user, 'Wall loops', 'wall loops');
+    expect(control.tagName).toBe('INPUT');
+  });
+});

+ 58 - 0
frontend/src/__tests__/utils/slicerPrinterMatch.test.ts

@@ -485,3 +485,61 @@ describe('presetCompatibility — nozzle-only @<size> tag (#2628 follow-up)', ()
     ).toBe('mismatch');
   });
 });
+
+describe("presetCompatibility — BambuStudio's \"# \" user-clone prefix", () => {
+  const index = buildCompatibilityIndex(PRINTER_MODELS);
+  // Editing a system preset saves a copy under this name; .bbscfg bundle
+  // exports use the same convention. The backend already normalises it in
+  // _canonical_printer_model.
+  const CLONED_X1C = '# Bambu Lab X1 Carbon 0.4 nozzle';
+
+  it('still matches a printer-tagged preset when the printer is a clone', () => {
+    // Regression: the prefix failed the "Bambu Lab …" test, so every preset
+    // came back 'unknown' and the dropdown filter silently did nothing.
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL X1C' }, 'process', CLONED_X1C, index),
+    ).toBe('match');
+  });
+
+  it('still rules out another printer when the selected printer is a clone', () => {
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL H2D' }, 'process', CLONED_X1C, index),
+    ).toBe('mismatch');
+  });
+
+  it('matches a cloned preset against an unprefixed printer', () => {
+    expect(
+      presetCompatibility({ name: '# 0.20mm Standard @BBL X1C' }, 'process', X1C, index),
+    ).toBe('match');
+  });
+
+  it('still compares the nozzle size through the prefix', () => {
+    expect(
+      presetCompatibility({ name: '0.20mm Standard @BBL X1C 0.6 nozzle' }, 'process', CLONED_X1C, index),
+    ).toBe('mismatch');
+  });
+
+  it('matches compatible_printers with the prefix on either side', () => {
+    // A preset cloned from a system printer lists the *unprefixed* name; a raw
+    // comparison against the "# " form reads as a mismatch, which now hides
+    // the preset rather than merely demoting it.
+    expect(
+      presetCompatibility({ name: 'My Process', compatible_printers: [X1C] }, 'process', CLONED_X1C, index),
+    ).toBe('match');
+    expect(
+      presetCompatibility({ name: 'My Process', compatible_printers: [CLONED_X1C] }, 'process', X1C, index),
+    ).toBe('match');
+  });
+
+  it('does not let the prefix turn a genuine mismatch into a match', () => {
+    expect(
+      presetCompatibility({ name: 'My Process', compatible_printers: [P2S] }, 'process', CLONED_X1C, index),
+    ).toBe('mismatch');
+  });
+
+  it('leaves an untagged clone unknown rather than guessing', () => {
+    expect(
+      presetCompatibility({ name: '# My own profile' }, 'process', CLONED_X1C, index),
+    ).toBe('unknown');
+  });
+});

+ 43 - 1
frontend/src/__tests__/utils/slicerToggle.test.ts

@@ -2,10 +2,12 @@ import { describe, it, expect } from 'vitest';
 
 import processSchema from '../../data/slicer/process-schema.json';
 import processToggles from '../../data/slicer/process-toggle-rules.json';
+import processTree from '../../data/slicer/process-ui-tree.json';
 import { disabledKeys, makeConfigReader } from '../../lib/slicerToggle';
-import type { ProcessSchema, SettingValue } from '../../types/slicerSettings';
+import type { ProcessSchema, ProcessUiTree, SettingValue } from '../../types/slicerSettings';
 
 const schema = processSchema as unknown as ProcessSchema;
+const tree = processTree as unknown as ProcessUiTree;
 const toggles = processToggles as { locals: Record<string, string>; rules: Array<{ fields: string[]; enable_if: string }> };
 
 const disabled = (settings: Record<string, SettingValue>) => disabledKeys(settings, schema, toggles);
@@ -93,3 +95,43 @@ describe('disabledKeys', () => {
     expect(decided.length).toBeGreaterThanOrEqual(Math.floor(toggles.rules.length * 0.6));
   });
 });
+
+describe('vendored process schema', () => {
+  // The extractor reads defaults and bounds out of C++ initialisers, so float
+  // literals arrive in source form — `0.`, `0.3f`, `100.%`, and `0.f` split
+  // into [0, "f"]. Rendering those verbatim put a column of "0." in the Line
+  // width group. scripts/generate-slicer-schema.mjs normalises them; this
+  // guards a regeneration that drops that step.
+  const LITERAL_ARTEFACT = /^-?[\d]*\.$|\.%$|^-?[\d.]+f$/;
+
+  const offenders = (field: 'default' | 'min' | 'max') =>
+    Object.entries(schema)
+      .filter(([, opt]) => {
+        const v = opt[field];
+        if (Array.isArray(v)) return v.some((x) => x === 'f');
+        return typeof v === 'string' && LITERAL_ARTEFACT.test(v);
+      })
+      .map(([key]) => `${key}.${field}`);
+
+  it.each(['default', 'min', 'max'] as const)('carries no C++ literal artefacts in %s', (field) => {
+    expect(offenders(field)).toEqual([]);
+  });
+
+  it('renders the line-width defaults as plain numbers', () => {
+    // The reported symptom: every field in this group showed "0."
+    for (const key of ['line_width', 'outer_wall_line_width', 'inner_wall_line_width', 'support_line_width']) {
+      expect(schema[key].default).toBe('0');
+    }
+    expect(schema.bridge_line_width.default).toBe('100%');
+  });
+
+  it('keeps every option the UI tree references', () => {
+    // A trim that drops a referenced key renders a control with no type,
+    // label or default.
+    for (const page of tree) {
+      for (const group of page.groups) {
+        for (const key of group.options) expect(schema[key]).toBeDefined();
+      }
+    }
+  });
+});

+ 120 - 99
frontend/src/components/SliceModal.tsx

@@ -1,5 +1,5 @@
 import { Cloud, CloudOff, Cog, Loader2, RefreshCw, X } from 'lucide-react';
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useId, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import {
@@ -17,7 +17,7 @@ import { useSliceJobTracker } from '../contexts/SliceJobTrackerContext';
 import { useToast } from '../contexts/ToastContext';
 import { useIsWideLayout } from '../hooks/useIsWideLayout';
 import { PlatePickerModal } from './PlatePickerModal';
-import SlicerSettingsPanel from './SlicerSettingsPanel';
+import SlicerSettingsPanel, { type FilamentChoice } from './SlicerSettingsPanel';
 import type { DesignOverride, PlateFilament } from '../types/plates';
 import type { SettingValue } from '../types/slicerSettings';
 import {
@@ -189,16 +189,6 @@ function formatElapsed(seconds: number): string {
   return `${h}h ${remM}m`;
 }
 
-// Render a slicer parameter value for the design-settings list. Bambu's process
-// schema stores everything as strings or arrays of strings, so this only has to
-// flatten arrays and keep scalars readable — no unit or type interpretation,
-// which would rot against every slicer release.
-function formatDesignValue(value: unknown): string {
-  if (Array.isArray(value)) return value.map((v) => String(v)).join(', ');
-  if (value == null) return '';
-  return String(value);
-}
-
 export function SliceModal({ source, onClose }: SliceModalProps) {
   const { t } = useTranslation();
   const { trackJob } = useSliceJobTracker();
@@ -256,7 +246,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // accelerations, prime-tower geometry) are listed but start unticked — those
   // were tuned for the designer's printer and can be plain wrong on another.
   const [designKeys, setDesignKeys] = useState<Set<string>>(new Set());
-  const [designExpanded, setDesignExpanded] = useState(false);
 
   // Process settings the user edited by hand in the settings panel. Two shapes
   // are kept: the panel's editing values, and the same set serialised into the
@@ -432,6 +421,24 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
     [printerModelsQuery.data],
   );
 
+  // Slot list for the settings panel's filament pickers (support base and
+  // interface, and the Multimaterial page's per-region options). Those store a
+  // plain integer, so without this the user has to map slot numbers onto their
+  // own AMS by hand. Falls back to the slot's material when a slot has no pick
+  // yet, so the list is never a column of blanks.
+  const filamentChoices = useMemo<FilamentChoice[]>(() => {
+    const data = presetsQuery.data;
+    return filamentSlots.map((slot, idx) => {
+      const ref = filamentPresets[idx] ?? null;
+      const preset = data && ref ? findPreset(data, ref, 'filament') : null;
+      return {
+        index: idx + 1,
+        label: preset?.name || slot.type || t('slice.filamentSlotUnset', 'not set'),
+        color: slot.color || undefined,
+      };
+    });
+  }, [filamentSlots, filamentPresets, presetsQuery.data, t]);
+
   // Printer / process preset names the source 3MF was prepared with. The
   // plates query resolves before the presets query (the latter is gated on
   // it), so these are known by the time the pre-pick effects run.
@@ -857,68 +864,6 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                 selectedPrinterName={selectedPrinterName}
                 compatIndex={compatIndex}
               />
-              {/* Designer's process tweaks (#2622). BambuStudio records which
-                  keys deviate from the stock preset in the 3MF itself, so a
-                  re-slice for another printer can carry them instead of
-                  flattening them under --load-settings. Hidden entirely when
-                  the source lists none, and disabled in embedded mode where
-                  the process JSON these patch is never sent. */}
-              {designOverrides.length > 0 && (
-                <div className="rounded-lg border border-bambu-dark-tertiary bg-bambu-dark/40 p-3">
-                  <button
-                    type="button"
-                    onClick={() => setDesignExpanded((v) => !v)}
-                    className="flex w-full items-center justify-between gap-2 text-left"
-                  >
-                    <span className="text-sm text-white">
-                      {t('slice.designSettings')}
-                      <span className="block text-xs text-bambu-gray/70">
-                        {t('slice.designSettingsHint', { count: designOverrides.length })}
-                      </span>
-                    </span>
-                    <span className="shrink-0 text-xs text-bambu-gray">
-                      {t('slice.designSettingsSelected', { selected: designKeys.size, total: designOverrides.length })}
-                    </span>
-                  </button>
-                  {designExpanded && (
-                    <div className="mt-3 space-y-1.5 border-t border-bambu-dark-tertiary pt-3">
-                      {designOverrides.map((o) => (
-                        <label
-                          key={o.key}
-                          className={`flex items-start gap-2 text-xs ${useEmbedded ? 'opacity-50' : 'cursor-pointer'}`}
-                        >
-                          <input
-                            type="checkbox"
-                            checked={designKeys.has(o.key)}
-                            disabled={isEnqueuing || useEmbedded}
-                            onChange={(e) => {
-                              setDesignKeys((prev) => {
-                                const next = new Set(prev);
-                                if (e.target.checked) next.add(o.key);
-                                else next.delete(o.key);
-                                return next;
-                              });
-                            }}
-                            className="mt-0.5 shrink-0 cursor-pointer"
-                          />
-                          <span className="min-w-0 flex-1">
-                            <span className="font-mono text-bambu-gray">{o.key}</span>
-                            <span className="ml-1.5 break-all text-white">{formatDesignValue(o.value)}</span>
-                            {o.printer_coupled && (
-                              <span
-                                className="ml-1.5 rounded bg-amber-100 px-1 py-0.5 text-[10px] text-amber-700 dark:bg-amber-500/20 dark:text-amber-400"
-                                title={t('slice.designSettingsPrinterCoupledHint')}
-                              >
-                                {t('slice.designSettingsPrinterCoupled')}
-                              </span>
-                            )}
-                          </span>
-                        </label>
-                      ))}
-                    </div>
-                  )}
-                </div>
-              )}
 
               {/* Bed-type override (#1337). Always visible, always enabled.
                   The backend patches curr_bed_type on the resolved process
@@ -1026,13 +971,17 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                 {/* Right column: the settings panel. It owns this column, so
                     there is nothing to collapse it out of the way of — the
                     disclosure below lg exists only because the single-column
-                    stack cannot afford 348 options unfolded. */}
+                    stack cannot afford 348 options unfolded.
+
+                    Kept on screen in embedded mode but disabled rather than
+                    removed: nothing here is sent on that path (the file's own
+                    settings drive the slice), and dropping the column outright
+                    made the dialog look like it had lost a feature whenever
+                    the toggle was flipped. */}
                 <div className="mt-4 lg:mt-0 min-w-0">
-                {/* Process settings, mirroring OrcaSlicer's own Print Settings
-                    tabs. Hidden entirely in embedded mode, where no process
-                    JSON is sent for these to patch. */}
-                {!useEmbedded && (
-                  <div className="rounded border border-bambu-dark-tertiary p-3">
+                  <div
+                    className={`rounded border border-bambu-dark-tertiary p-3 ${useEmbedded ? 'opacity-60' : ''}`}
+                  >
                     <button
                       type="button"
                       onClick={() => setSettingsExpanded((v) => !v)}
@@ -1043,15 +992,25 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                       <span className="text-sm text-white">
                         {t('slice.processSettings', 'Process settings')}
                         <span className="block text-xs text-bambu-gray/70">
-                          {t('slice.processSettingsHint', "Adjust the picked preset for this slice. Anything you don't touch stays as the preset defines it.")}
+                          {useEmbedded
+                            ? t(
+                                'slice.processSettingsEmbedded',
+                                "Not used while \"Use the file's built-in settings\" is on -- the file's own settings drive this slice.",
+                              )
+                            : t(
+                                'slice.processSettingsHint',
+                                "Adjust the picked preset for this slice. Anything you don't touch stays as the preset defines it.",
+                              )}
                         </span>
                       </span>
                       <span className="shrink-0 text-xs text-bambu-gray">
-                        {Object.keys(serializedProcessOverrides).length > 0
-                          ? t('slice.processSettingsChanged', '{{count}} changed', {
-                              count: Object.keys(serializedProcessOverrides).length,
-                            })
-                          : t('slice.processSettingsUnchanged', 'Preset defaults')}
+                        {useEmbedded
+                          ? t('slice.processSettingsInactive', 'Inactive')
+                          : Object.keys(serializedProcessOverrides).length > 0
+                            ? t('slice.processSettingsChanged', '{{count}} changed', {
+                                count: Object.keys(serializedProcessOverrides).length,
+                              })
+                            : t('slice.processSettingsUnchanged', 'Preset defaults')}
                       </span>
                     </button>
                     {panelOpen && (
@@ -1062,12 +1021,28 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                             setProcessOverrides(values);
                             setSerializedProcessOverrides(serialized);
                           }}
-                          disabled={isEnqueuing}
+                          disabled={isEnqueuing || useEmbedded}
+                          // The designer's own deviations (#2622) are shown
+                          // against the options they belong to rather than in
+                          // a list of their own. Only the tick state lives
+                          // here; the values still travel as design_overrides,
+                          // so the backend keeps reading them from the file
+                          // and keys outside the vendored schema stay faithful.
+                          filamentChoices={filamentChoices}
+                          sourceOverrides={designOverrides}
+                          sourceSelected={designKeys}
+                          onToggleSource={(key, on) =>
+                            setDesignKeys((prev) => {
+                              const next = new Set(prev);
+                              if (on) next.add(key);
+                              else next.delete(key);
+                              return next;
+                            })
+                          }
                         />
                       </div>
                     )}
                   </div>
-                )}
                 </div>
               </div>
             </>
@@ -1259,8 +1234,8 @@ interface PresetDropdownProps {
   // configuring against the source 3MF's per-slot colour.
   swatchColor?: string;
   // Selected printer context (#1325). When provided for a process / filament
-  // slot, presets that resolve to a different printer (per compatIndex) move
-  // into a trailing "Other printers" group instead of the main tier list.
+  // slot, presets that resolve to a different printer (per compatIndex) are
+  // held back behind a "Show all" link instead of padding out the main list.
   selectedPrinterName?: string | null;
   compatIndex?: PrinterCompatibilityIndex;
 }
@@ -1277,6 +1252,14 @@ function PresetDropdown({
   compatIndex,
 }: PresetDropdownProps) {
   const { t } = useTranslation();
+  // Reveals the other-printer group for this slot only. Per-dropdown rather
+  // than modal-wide: wanting a filament from another printer's library says
+  // nothing about wanting its process profiles too.
+  const [showAll, setShowAll] = useState(false);
+  // Binds the label to the select now that they are siblings rather than
+  // nested. Filament slots render several of these, so the id must be unique
+  // per instance rather than derived from the slot name.
+  const selectId = useId();
 
   // Tier sections (imported → cloud → standard), plus — for a process /
   // filament slot with a selected printer — a trailing group of presets that
@@ -1322,12 +1305,30 @@ function PresetDropdown({
     return { sections: compatSections, otherEntries: other };
   }, [data, slot, t, selectedPrinterName, compatIndex]);
 
+  // Other-printer presets are held back by default so the list shows what is
+  // usable on the selected printer. Two things are never hidden: a preset whose
+  // compatibility is merely *unknown* (it never reaches otherEntries), and the
+  // one currently selected — a pipeline or an auto-pick can land on a
+  // cross-printer preset, and dropping it from the options would blank the
+  // select and silently discard the choice.
+  const selectedRefValue = toRefValue(value);
+  const visibleOther = useMemo(() => {
+    if (showAll) return otherEntries;
+    return otherEntries.filter((p) => `${p.source}:${p.id}` === selectedRefValue);
+  }, [showAll, otherEntries, selectedRefValue]);
+
+  const hiddenCount = otherEntries.length - visibleOther.length;
   const totalEntries =
-    sections.reduce((sum, s) => sum + s.entries.length, 0) + otherEntries.length;
+    sections.reduce((sum, s) => sum + s.entries.length, 0) + visibleOther.length;
 
   return (
-    <label className="block">
-      <span className="flex items-center gap-2 text-xs text-bambu-gray mb-1">
+    // A plain wrapper rather than a <label> around everything: the "Show all"
+    // control is a button, and a button inside a label that also wraps the
+    // select inherits the whole label as its accessible name (screen readers
+    // announced it as "Process profile 2 hidden 0.20mm Standard @BBL X1C") as
+    // well as being invalid HTML. The label is bound to the select by id.
+    <div className="block">
+      <div className="flex items-center gap-2 text-xs text-bambu-gray mb-1">
         {swatchColor && (
           <span
             className="inline-block w-3 h-3 rounded-full border border-bambu-dark-tertiary"
@@ -1335,9 +1336,29 @@ function PresetDropdown({
             aria-hidden
           />
         )}
-        <span>{label}</span>
-      </span>
+        <label htmlFor={selectId}>{label}</label>
+        {(hiddenCount > 0 || showAll) && (
+          <span className="ml-auto flex items-center gap-1.5 font-normal">
+            {hiddenCount > 0 && (
+              <span className="text-bambu-gray/60">
+                {t('slice.presetsHidden', '{{count}} hidden', { count: hiddenCount })}
+              </span>
+            )}
+            <button
+              type="button"
+              onClick={() => setShowAll((v) => !v)}
+              disabled={disabled}
+              className="text-bambu-green hover:underline disabled:opacity-50 disabled:no-underline"
+            >
+              {showAll
+                ? t('slice.showFewerPresets', 'Show fewer')
+                : t('slice.showAllPresets', 'Show all')}
+            </button>
+          </span>
+        )}
+      </div>
       <select
+        id={selectId}
         value={toRefValue(value)}
         onChange={(e) => onChange(fromRefValue(e.target.value))}
         disabled={disabled || totalEntries === 0}
@@ -1357,9 +1378,9 @@ function PresetDropdown({
             ))}
           </optgroup>
         ))}
-        {otherEntries.length > 0 && (
+        {visibleOther.length > 0 && (
           <optgroup label={t('slice.otherPrinters')}>
-            {otherEntries.map((p) => (
+            {visibleOther.map((p) => (
               <option key={`${p.source}:${p.id}`} value={`${p.source}:${p.id}`}>
                 {p.name}
               </option>
@@ -1367,6 +1388,6 @@ function PresetDropdown({
           </optgroup>
         )}
       </select>
-    </label>
+    </div>
   );
 }

+ 260 - 28
frontend/src/components/SlicerSettingsPanel.tsx

@@ -20,11 +20,12 @@
 
 import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Search, RotateCcw, Loader2 } from 'lucide-react';
+import { Search, RotateCcw, Loader2, ChevronDown } from 'lucide-react';
 
 import { disabledKeys, type ToggleRules } from '../lib/slicerToggle';
 import { defaultForDisplay, displaySidetext, isModified, numericBound, serializeOverrides } from '../lib/slicerSettings';
 import type { OptionMode, ProcessOption, ProcessSchema, ProcessUiTree, SettingValue } from '../types/slicerSettings';
+import type { DesignOverride } from '../types/plates';
 
 interface SlicerData {
   schema: ProcessSchema;
@@ -45,13 +46,73 @@ interface Props {
    */
   onChange: (values: Record<string, SettingValue>, serialized: Record<string, string | string[]>) => void;
   disabled?: boolean;
+  /**
+   * Process settings the source 3MF's designer moved off the stock preset
+   * (#2622), as recorded by BambuStudio in `different_settings_to_system`.
+   *
+   * These are shown inline against the options they belong to rather than in a
+   * list of their own, so there is one place to see what this slice will use.
+   * Their *values* are not routed through this component: the backend reads
+   * them straight out of the file, which keeps settings faithful even for keys
+   * outside the option schema we vendor. All this panel decides is which of
+   * them are switched on.
+   */
+  sourceOverrides?: DesignOverride[];
+  /** Which source-override keys are currently switched on. */
+  sourceSelected?: Set<string>;
+  onToggleSource?: (key: string, on: boolean) => void;
+  /**
+   * The filaments picked on the slice dialog's left-hand side, in slot order.
+   *
+   * A handful of options select *which filament* prints a given feature —
+   * supports, outer walls, infill. The slicer stores those as a plain integer
+   * where 0 means "whatever filament the region already uses" and 1..N is a
+   * slot. A bare number field makes the user count their own AMS slots, so
+   * when this is supplied those options become a dropdown of the actual
+   * picks instead.
+   */
+  filamentChoices?: FilamentChoice[];
 }
 
+export interface FilamentChoice {
+  /** 1-based slot index, matching the integer the slicer stores. */
+  index: number;
+  /** Preset name, or a fallback when the slot has no pick yet. */
+  label: string;
+  /** Slot colour from the source plate, for the swatch. */
+  color?: string;
+}
+
+/**
+ * Options whose integer value names a filament slot rather than a quantity.
+ * All use the same encoding: 0 = "default / current filament", 1..N = slot.
+ * Support base and interface are the pair on the Support page; the rest are
+ * the Multimaterial page's per-region pickers, which have the same wart.
+ */
+const FILAMENT_SLOT_OPTIONS = new Set([
+  'support_filament',
+  'support_interface_filament',
+  'outer_wall_filament_id',
+  'inner_wall_filament_id',
+  'top_surface_filament_id',
+  'bottom_surface_filament_id',
+  'internal_solid_filament_id',
+  'sparse_infill_filament_id',
+]);
+
 /** Visibility tiers, in increasing order of how much they reveal. */
 const MODES: OptionMode[] = ['simple', 'advanced', 'expert'];
 const MODE_RANK: Record<string, number> = { simple: 0, advanced: 1, expert: 2, develop: 3 };
 
-export default function SlicerSettingsPanel({ values, onChange, disabled = false }: Props) {
+export default function SlicerSettingsPanel({
+  values,
+  onChange,
+  disabled = false,
+  sourceOverrides = [],
+  sourceSelected,
+  onToggleSource,
+  filamentChoices,
+}: Props) {
   const { t } = useTranslation();
   const [data, setData] = useState<SlicerData | null>(null);
   const [mode, setMode] = useState<OptionMode>('simple');
@@ -84,6 +145,19 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
     [data, values],
   );
 
+  const sourceByKey = useMemo(
+    () => new Map(sourceOverrides.map((o) => [o.key, o])),
+    [sourceOverrides],
+  );
+
+  // 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.
+  const unlistedSource = useMemo(() => {
+    if (!data) return [];
+    return sourceOverrides.filter((o) => !data.schema[o.key]);
+  }, [data, sourceOverrides]);
+
   const emit = (next: Record<string, SettingValue>) => {
     if (!data) return;
     // Only genuine deviations are worth sending: an override that equals the
@@ -106,19 +180,30 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
   // Search cuts across every page; without a query we show the selected page.
   const visiblePages = useMemo(() => {
     if (!data) return [];
-    const needle = query.trim().toLowerCase();
+    // Underscores and spaces are interchangeable so "outer wall speed" finds
+    // `outer_wall_speed`. That matters more than it looks: several labels are
+    // only meaningful with their group ("Outer wall" under Speed), so the key
+    // is often the only place the full phrase appears.
+    const flatten = (s: string) => s.toLowerCase().replace(/[_\s]+/g, ' ').trim();
+    const needle = flatten(query);
     const withinMode = (key: string) => MODE_RANK[data.schema[key]?.mode ?? 'expert'] <= MODE_RANK[mode];
-    const matches = (key: string) => {
+    const matches = (key: string, group: string, page: string) => {
       if (!needle) return true;
       const opt = data.schema[key];
-      return key.includes(needle) || opt?.label?.toLowerCase().includes(needle) || opt?.tooltip?.toLowerCase().includes(needle);
+      // Group and page are matched too, so "speed" lists the Speed page's
+      // options rather than only the handful with "speed" in their label.
+      const haystack = [key, opt?.label ?? '', opt?.tooltip ?? '', group, page];
+      return haystack.some((h) => flatten(h).includes(needle));
     };
 
     return data.tree
       .map((p) => ({
         ...p,
         groups: p.groups
-          .map((g) => ({ ...g, options: g.options.filter((k) => withinMode(k) && matches(k)) }))
+          .map((g) => ({
+            ...g,
+            options: g.options.filter((k) => withinMode(k) && matches(k, g.group, p.page)),
+          }))
           .filter((g) => g.options.length > 0),
       }))
       .filter((p) => p.groups.length > 0);
@@ -149,7 +234,7 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
   return (
     <div className="flex flex-col gap-3">
       <div className="flex flex-wrap items-center gap-2">
-        <div className="flex rounded overflow-hidden border border-white/10">
+        <div className="flex overflow-hidden rounded border border-bambu-dark-tertiary">
           {MODES.map((m) => (
             <button
               key={m}
@@ -157,7 +242,7 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
               onClick={() => setMode(m)}
               disabled={disabled}
               className={`px-2.5 py-1 text-xs capitalize transition-colors ${
-                mode === m ? 'bg-bambu-green text-black' : 'text-bambu-gray hover:text-white'
+                mode === m ? 'bg-bambu-green text-white' : 'text-bambu-gray hover:text-white'
               }`}
             >
               {t(`slicerSettings.mode.${m}`, m)}
@@ -173,7 +258,7 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
             onChange={(e) => setQuery(e.target.value)}
             disabled={disabled}
             placeholder={t('slicerSettings.searchPlaceholder', 'Search settings')}
-            className="w-full bg-black/30 border border-white/10 rounded pl-7 pr-2 py-1 text-xs text-white placeholder:text-bambu-gray/60"
+            className="w-full rounded border border-bambu-dark-tertiary bg-bambu-dark pl-7 pr-2 py-1 text-xs text-white placeholder:text-bambu-gray/60 focus:border-bambu-green focus:outline-none disabled:opacity-40"
           />
         </div>
 
@@ -199,7 +284,7 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
               onClick={() => setPage(p.page)}
               disabled={disabled}
               className={`px-2 py-1 text-xs rounded transition-colors ${
-                activePage?.page === p.page ? 'bg-white/10 text-white' : 'text-bambu-gray hover:text-white'
+                activePage?.page === p.page ? 'bg-bambu-dark-tertiary text-white' : 'text-bambu-gray hover:text-white'
               }`}
             >
               {p.page}
@@ -221,7 +306,7 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
               {query.trim() && <p className="text-[0.7rem] uppercase tracking-wide text-bambu-gray/70">{p.page}</p>}
               {p.groups.map((g) => (
                 <fieldset key={`${p.page}:${g.group}`} className="flex flex-col gap-1.5">
-                  <legend className="text-xs font-medium text-white/80 mb-1">{g.group}</legend>
+                  <legend className="mb-1 text-xs font-medium text-white">{g.group}</legend>
                   {g.options.map((key) => (
                     <OptionRow
                       key={key}
@@ -231,12 +316,48 @@ export default function SlicerSettingsPanel({ values, onChange, disabled = false
                       onChange={(v) => setValue(key, v)}
                       disabled={disabled || off.has(key)}
                       disabledBySlicer={off.has(key)}
+                      source={sourceByKey.get(key)}
+                      sourceOn={sourceSelected?.has(key) ?? false}
+                      onToggleSource={onToggleSource}
+                      filamentChoices={FILAMENT_SLOT_OPTIONS.has(key) ? filamentChoices : undefined}
                     />
                   ))}
                 </fieldset>
               ))}
             </div>
           ))}
+
+          {/* Source-file settings the vendored schema has no entry for: they
+              still apply (the backend reads their values from the file), so
+              they get a plain key/value group rather than disappearing from a
+              panel that claims to show what this slice will use. */}
+          {unlistedSource.length > 0 && !query.trim() && (
+            <fieldset className="flex flex-col gap-1.5">
+              <legend className="mb-1 text-xs font-medium text-white">
+                {t('slicerSettings.otherFromFile', 'Other settings from this file')}
+              </legend>
+              {unlistedSource.map((o) => (
+                <label key={o.key} className="flex items-center gap-2 text-xs cursor-pointer">
+                  <input
+                    type="checkbox"
+                    checked={sourceSelected?.has(o.key) ?? false}
+                    disabled={disabled || !onToggleSource}
+                    onChange={(e) => onToggleSource?.(o.key, e.target.checked)}
+                    className="shrink-0 cursor-pointer disabled:opacity-40"
+                  />
+                  <span className="min-w-0 flex-1 truncate">
+                    <span className="font-mono text-bambu-gray">{o.key}</span>
+                    <span className="ml-1.5 text-white">{formatSourceValue(o.value)}</span>
+                  </span>
+                  {o.printer_coupled && (
+                    <span className="shrink-0 rounded bg-amber-100 px-1 py-0.5 text-[10px] text-amber-700 dark:bg-amber-500/20 dark:text-amber-400">
+                      {t('slicerSettings.fromFilePrinterCoupled', "designer's printer")}
+                    </span>
+                  )}
+                </label>
+              ))}
+            </fieldset>
+          )}
         </div>
       )}
     </div>
@@ -251,13 +372,37 @@ interface RowProps {
   disabled: boolean;
   /** Greyed because the slicer's own rules turn it off, not because the form is busy. */
   disabledBySlicer: boolean;
+  /** Set when the source file's designer moved this option off the stock preset. */
+  source?: DesignOverride;
+  sourceOn?: boolean;
+  onToggleSource?: (key: string, on: boolean) => void;
+  /** Set only for options whose integer value names a filament slot. */
+  filamentChoices?: FilamentChoice[];
 }
 
-function OptionRow({ optionKey, option, value, onChange, disabled, disabledBySlicer }: RowProps) {
+function OptionRow({
+  optionKey,
+  option,
+  value,
+  onChange,
+  disabled,
+  disabledBySlicer,
+  source,
+  sourceOn = false,
+  onToggleSource,
+  filamentChoices,
+}: RowProps) {
   const { t } = useTranslation();
   const modified = isModified(option, value);
   const unit = displaySidetext(option);
-  const current = value === undefined ? defaultForDisplay(option) : String(value);
+  // What this slice will actually use, in precedence order: a value typed here
+  // wins, then the designer's value if it is switched on, then the preset's.
+  const current =
+    value !== undefined
+      ? String(value)
+      : sourceOn && source
+        ? formatSourceValue(source.value)
+        : defaultForDisplay(option);
 
   return (
     <div className="flex items-center gap-2 group" title={option.tooltip}>
@@ -267,6 +412,24 @@ function OptionRow({ optionKey, option, value, onChange, disabled, disabledBySli
       >
         {option.label || optionKey}
         {modified && <span className="ml-1 text-bambu-green" aria-hidden="true">•</span>}
+        {source && (
+          <span
+            className={`ml-1.5 rounded px-1 py-0.5 text-[10px] ${
+              source.printer_coupled
+                ? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400'
+                : 'bg-bambu-green/15 text-bambu-green'
+            }`}
+            title={
+              source.printer_coupled
+                ? t('slicerSettings.fromFilePrinterCoupledHint', "Tuned for the printer this file was designed for -- may be wrong or out of range on yours.")
+                : t('slicerSettings.fromFileHint', "The designer changed this in the source file. Its value is {{value}}.", { value: formatSourceValue(source.value) })
+            }
+          >
+            {source.printer_coupled
+              ? t('slicerSettings.fromFilePrinterCoupled', "designer's printer")
+              : t('slicerSettings.fromFile', 'from file')}
+          </span>
+        )}
       </label>
 
       <div className="flex items-center gap-1 shrink-0">
@@ -276,8 +439,24 @@ function OptionRow({ optionKey, option, value, onChange, disabled, disabledBySli
           current={current}
           onChange={onChange}
           disabled={disabled}
+          filamentChoices={filamentChoices}
         />
         {unit && <span className="text-[0.65rem] text-bambu-gray/60 w-10 truncate">{unit}</span>}
+        {source && onToggleSource && (
+          <input
+            type="checkbox"
+            checked={sourceOn}
+            disabled={disabled}
+            onChange={(e) => onToggleSource(optionKey, e.target.checked)}
+            aria-label={t('slicerSettings.useFromFile', "Use the source file's value for {{option}}", {
+              option: option.label || optionKey,
+            })}
+            title={t('slicerSettings.useFromFile', "Use the source file's value for {{option}}", {
+              option: option.label || optionKey,
+            })}
+            className="w-3 h-3 cursor-pointer disabled:opacity-40"
+          />
+        )}
         <button
           type="button"
           onClick={() => onChange(undefined)}
@@ -292,16 +471,63 @@ function OptionRow({ optionKey, option, value, onChange, disabled, disabledBySli
   );
 }
 
+/**
+ * Render a value read out of the source file. Bambu's process config stores
+ * everything as strings or arrays of strings, so this only has to flatten
+ * arrays — no unit or type interpretation, which would rot against every
+ * slicer release.
+ */
+function formatSourceValue(value: unknown): string {
+  if (Array.isArray(value)) return value.map((v) => String(v)).join(', ');
+  if (value == null) return '';
+  return String(value);
+}
+
 interface ControlProps {
   id: string;
   option: ProcessOption;
   current: string;
   onChange: (value: SettingValue | undefined) => void;
   disabled: boolean;
+  filamentChoices?: FilamentChoice[];
 }
 
-function OptionControl({ id, option, current, onChange, disabled }: ControlProps) {
-  const inputClass = 'bg-black/30 border border-white/10 rounded px-1.5 py-0.5 text-xs text-white disabled:opacity-40 w-24';
+function OptionControl({ id, option, current, onChange, disabled, filamentChoices }: ControlProps) {
+  const { t } = useTranslation();
+  // Theme tokens rather than raw black/white: bambu-dark and
+  // bambu-dark-tertiary are CSS variables that follow the active theme, and
+  // `text-white` is remapped to --text-primary in index.css.
+  const inputClass =
+    'w-24 rounded border border-bambu-dark-tertiary bg-bambu-dark px-1.5 py-0.5 text-xs text-white focus:border-bambu-green focus:outline-none disabled:opacity-40';
+
+  // Filament-slot pickers come before the generic branches: the value is an
+  // integer, but offering a spinner over "1, 2, 3" makes the user map slot
+  // numbers to their own AMS by hand.
+  if (filamentChoices && filamentChoices.length > 0) {
+    const selected = filamentChoices.find((c) => String(c.index) === current);
+    return (
+      <div className="relative w-24">
+        <select
+          id={id}
+          value={current}
+          onChange={(e) => onChange(e.target.value)}
+          disabled={disabled}
+          className={`${inputClass} w-full cursor-pointer appearance-none pr-5`}
+          // The full name rarely fits in the control, so the hover carries it.
+          title={selected?.label}
+        >
+          {/* 0 is the slicer's "no specific filament — use the region's own". */}
+          <option value="0">{t('slicerSettings.filamentDefault', 'Default')}</option>
+          {filamentChoices.map((choice) => (
+            <option key={choice.index} value={String(choice.index)}>
+              {choice.index}: {choice.label}
+            </option>
+          ))}
+        </select>
+        <ChevronDown className="pointer-events-none absolute right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 text-bambu-gray" />
+      </div>
+    );
+  }
 
   if (option.type === 'coBool') {
     return (
@@ -317,20 +543,26 @@ function OptionControl({ id, option, current, onChange, disabled }: ControlProps
   }
 
   if (option.type === 'coEnum' && option.enum_values) {
+    // Native select chrome is replaced the same way as everywhere else in
+    // Bambuddy: appearance-none plus our own chevron, so the control matches
+    // the app in both themes instead of whatever the browser paints.
     return (
-      <select
-        id={id}
-        value={current}
-        onChange={(e) => onChange(e.target.value)}
-        disabled={disabled}
-        className={inputClass}
-      >
-        {option.enum_values.map((v, i) => (
-          <option key={v} value={v}>
-            {option.enum_labels?.[i] ?? v}
-          </option>
-        ))}
-      </select>
+      <div className="relative w-24">
+        <select
+          id={id}
+          value={current}
+          onChange={(e) => onChange(e.target.value)}
+          disabled={disabled}
+          className={`${inputClass} w-full cursor-pointer appearance-none pr-5`}
+        >
+          {option.enum_values.map((v, i) => (
+            <option key={v} value={v}>
+              {option.enum_labels?.[i] ?? v}
+            </option>
+          ))}
+        </select>
+        <ChevronDown className="pointer-events-none absolute right-1.5 top-1/2 h-3 w-3 -translate-y-1/2 text-bambu-gray" />
+      </div>
     );
   }
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
frontend/src/data/slicer/process-schema.json


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
frontend/src/data/slicer/process-toggle-rules.json


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
frontend/src/data/slicer/process-ui-tree.json


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

@@ -4279,6 +4279,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'Standard',
+    fromFile: 'aus Datei',
+    fromFileHint: 'Der Designer hat dies in der Quelldatei geändert. Wert: {{value}}.',
+    fromFilePrinterCoupled: 'Drucker des Designers',
+    fromFilePrinterCoupledHint: 'Auf den Drucker abgestimmt, für den diese Datei erstellt wurde – auf Ihrem kann der Wert falsch oder außerhalb des Bereichs sein.',
+    useFromFile: 'Wert aus der Quelldatei für {{option}} verwenden',
+    otherFromFile: 'Weitere Einstellungen aus dieser Datei',
     loading: 'Slicer-Einstellungen werden geladen…',
     mode: {
       simple: 'Einfach',
@@ -4291,6 +4298,12 @@ export default {
     noMatches: 'Keine Einstellungen passen zu dieser Suche.',
   },
   slice: {
+    filamentSlotUnset: 'nicht gewählt',
+    processSettingsEmbedded: 'Wird nicht verwendet, solange „Integrierte Einstellungen der Datei verwenden“ aktiv ist – die Einstellungen der Datei bestimmen diesen Slice.',
+    processSettingsInactive: 'Inaktiv',
+    presetsHidden: '{{count}} ausgeblendet',
+    showAllPresets: 'Alle anzeigen',
+    showFewerPresets: 'Weniger anzeigen',
     processSettings: 'Prozesseinstellungen',
     processSettingsHint: 'Passen Sie das gewählte Profil für diesen Slice an. Alles, was Sie nicht ändern, bleibt wie im Profil definiert.',
     processSettingsChanged: '{{count}} geändert',
@@ -4324,11 +4337,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'druckerspezifisch',
-    designSettingsPrinterCoupledHint: 'Auf den Drucker abgestimmt, für den die Datei erstellt wurde — auf deinem kann der Wert falsch oder unzulässig sein.',
     enqueuing: 'Slice-Auftrag wird übermittelt…',
     queued: 'In Warteschlange…',
     failed: 'Slicen fehlgeschlagen. Logs des Slicer-Sidecars prüfen.',

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

@@ -4313,6 +4313,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'Default',
+    fromFile: 'from file',
+    fromFileHint: 'The designer changed this in the source file. Its value is {{value}}.',
+    fromFilePrinterCoupled: "designer's printer",
+    fromFilePrinterCoupledHint: 'Tuned for the printer this file was designed for -- may be wrong or out of range on yours.',
+    useFromFile: "Use the source file's value for {{option}}",
+    otherFromFile: 'Other settings from this file',
     loading: 'Loading slicer settings…',
     mode: {
       simple: 'Simple',
@@ -4325,6 +4332,12 @@ export default {
     noMatches: 'No settings match this search.',
   },
   slice: {
+    filamentSlotUnset: 'not set',
+    processSettingsEmbedded: 'Not used while "Use the file\'s built-in settings" is on -- the file\'s own settings drive this slice.',
+    processSettingsInactive: 'Inactive',
+    presetsHidden: '{{count}} hidden',
+    showAllPresets: 'Show all',
+    showFewerPresets: 'Show fewer',
     processSettings: 'Process settings',
     processSettingsHint: "Adjust the picked preset for this slice. Anything you don't touch stays as the preset defines it.",
     processSettingsChanged: '{{count}} changed',
@@ -4358,11 +4371,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'printer-specific',
-    designSettingsPrinterCoupledHint: 'Tuned for the printer this file was designed for — may be wrong or out of range on yours.',
     enqueuing: 'Submitting slice job…',
     queued: 'Queued…',
     failed: 'Slicing failed. Check the slicer sidecar logs.',

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

@@ -4281,6 +4281,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'Predeterminado',
+    fromFile: 'del archivo',
+    fromFileHint: 'El diseñador cambió esto en el archivo de origen. Su valor es {{value}}.',
+    fromFilePrinterCoupled: 'impresora del diseñador',
+    fromFilePrinterCoupledHint: 'Ajustado para la impresora para la que se diseñó este archivo: en la tuya puede ser incorrecto o estar fuera de rango.',
+    useFromFile: 'Usar el valor del archivo de origen para {{option}}',
+    otherFromFile: 'Otros ajustes de este archivo',
     loading: 'Cargando ajustes del laminador…',
     mode: {
       simple: 'Simple',
@@ -4293,6 +4300,12 @@ export default {
     noMatches: 'Ningún ajuste coincide con esta búsqueda.',
   },
   slice: {
+    filamentSlotUnset: 'sin definir',
+    processSettingsEmbedded: 'No se usa mientras «Usar los ajustes integrados del archivo» está activo: los ajustes del propio archivo rigen este corte.',
+    processSettingsInactive: 'Inactivo',
+    presetsHidden: '{{count}} ocultos',
+    showAllPresets: 'Mostrar todos',
+    showFewerPresets: 'Mostrar menos',
     processSettings: 'Ajustes de proceso',
     processSettingsHint: 'Ajusta el perfil seleccionado para este corte. Todo lo que no toques se mantiene como lo define el perfil.',
     processSettingsChanged: '{{count}} cambiados',
@@ -4326,11 +4339,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'específico de la impresora',
-    designSettingsPrinterCoupledHint: 'Ajustado para la impresora para la que se diseñó el archivo: puede ser incorrecto o quedar fuera de rango en la tuya.',
     enqueuing: 'Enviando el trabajo de laminado…',
     queued: 'En cola…',
     failed: 'Error al laminar. Consulte los registros del contenedor auxiliar del laminador.',

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

@@ -4268,6 +4268,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'Par défaut',
+    fromFile: 'du fichier',
+    fromFileHint: 'Le concepteur a modifié ce paramètre dans le fichier source. Sa valeur est {{value}}.',
+    fromFilePrinterCoupled: 'imprimante du concepteur',
+    fromFilePrinterCoupledHint: "Réglé pour l'imprimante pour laquelle ce fichier a été conçu — peut être incorrect ou hors plage sur la vôtre.",
+    useFromFile: 'Utiliser la valeur du fichier source pour {{option}}',
+    otherFromFile: 'Autres paramètres de ce fichier',
     loading: 'Chargement des paramètres du trancheur…',
     mode: {
       simple: 'Simple',
@@ -4280,6 +4287,12 @@ export default {
     noMatches: 'Aucun paramètre ne correspond à cette recherche.',
   },
   slice: {
+    filamentSlotUnset: 'non défini',
+    processSettingsEmbedded: 'Inutilisé tant que « Utiliser les paramètres intégrés du fichier » est activé : ce sont les paramètres du fichier qui pilotent ce découpage.',
+    processSettingsInactive: 'Inactif',
+    presetsHidden: '{{count}} masqués',
+    showAllPresets: 'Tout afficher',
+    showFewerPresets: 'Afficher moins',
     processSettings: 'Paramètres de process',
     processSettingsHint: 'Ajustez le profil choisi pour ce découpage. Tout ce que vous ne modifiez pas reste tel que défini par le profil.',
     processSettingsChanged: '{{count}} modifiés',
@@ -4313,11 +4326,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'spécifique à l\'imprimante',
-    designSettingsPrinterCoupledHint: 'Réglé pour l\'imprimante visée par le fichier — peut être incorrect ou hors plage sur la vôtre.',
     enqueuing: 'Envoi du travail de découpage…',
     queued: 'En file d\'attente…',
     failed: 'Échec du découpage. Vérifiez les journaux du sidecar.',

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

@@ -4267,6 +4267,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'Predefinito',
+    fromFile: 'dal file',
+    fromFileHint: 'Il designer ha modificato questo parametro nel file di origine. Il valore è {{value}}.',
+    fromFilePrinterCoupled: 'stampante del designer',
+    fromFilePrinterCoupledHint: 'Tarato per la stampante per cui è stato progettato questo file: sulla tua può essere errato o fuori intervallo.',
+    useFromFile: 'Usa il valore del file di origine per {{option}}',
+    otherFromFile: 'Altre impostazioni da questo file',
     loading: 'Caricamento impostazioni dello slicer…',
     mode: {
       simple: 'Semplice',
@@ -4279,6 +4286,12 @@ export default {
     noMatches: 'Nessuna impostazione corrisponde a questa ricerca.',
   },
   slice: {
+    filamentSlotUnset: 'non impostato',
+    processSettingsEmbedded: 'Non utilizzato finché «Usa le impostazioni integrate del file» è attivo: sono le impostazioni del file a guidare questo slice.',
+    processSettingsInactive: 'Non attivo',
+    presetsHidden: '{{count}} nascosti',
+    showAllPresets: 'Mostra tutti',
+    showFewerPresets: 'Mostra meno',
     processSettings: 'Impostazioni di processo',
     processSettingsHint: 'Regola il profilo scelto per questo slice. Tutto ciò che non tocchi resta come definito dal profilo.',
     processSettingsChanged: '{{count}} modificate',
@@ -4312,11 +4325,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'specifico della stampante',
-    designSettingsPrinterCoupledHint: 'Tarato per la stampante per cui è stato progettato il file: sulla tua può essere errato o fuori intervallo.',
     enqueuing: 'Invio lavoro di slicing…',
     queued: 'In coda…',
     failed: 'Slicing fallito. Controlla i log del sidecar.',

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

@@ -4279,6 +4279,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: '既定',
+    fromFile: 'ファイル由来',
+    fromFileHint: 'この項目は元ファイルで設計者が変更しています。値は {{value}} です。',
+    fromFilePrinterCoupled: '設計者のプリンター',
+    fromFilePrinterCoupledHint: 'このファイルが設計されたプリンター向けの値です。お使いのプリンターでは不適切または範囲外になる場合があります。',
+    useFromFile: '{{option}} に元ファイルの値を使用する',
+    otherFromFile: 'このファイルのその他の設定',
     loading: 'スライサー設定を読み込んでいます…',
     mode: {
       simple: 'シンプル',
@@ -4291,6 +4298,12 @@ export default {
     noMatches: 'この検索に一致する設定はありません。',
   },
   slice: {
+    filamentSlotUnset: '未設定',
+    processSettingsEmbedded: '「ファイル内蔵の設定を使用」が有効な間は使用されません。このスライスはファイル自身の設定で実行されます。',
+    processSettingsInactive: '無効',
+    presetsHidden: '{{count}} 件を非表示',
+    showAllPresets: 'すべて表示',
+    showFewerPresets: '表示を減らす',
     processSettings: 'プロセス設定',
     processSettingsHint: 'このスライス用に選択したプリセットを調整します。変更しない項目はプリセットの定義のままです。',
     processSettingsChanged: '{{count}} 件変更',
@@ -4324,11 +4337,6 @@ export default {
     autoOrientHint: 'スライスする前に、各オブジェクトを印刷に適した面へ自動で回転させます。ファイルに保存された向きは上書きされます。',
     autoArrange: 'プレート上に自動配置',
     autoArrangeHint: 'オブジェクトが重ならないようにスライサーが並べ直します。ファイルの配置は置き換えられます。',
-    designSettings: '設計者の設定を保持',
-    designSettingsHint: 'このファイルは標準プロファイルから {{count}} 個の印刷設定を変更しています。',
-    designSettingsSelected: '{{total}} 個中 {{selected}} 個を選択',
-    designSettingsPrinterCoupled: 'プリンター固有',
-    designSettingsPrinterCoupledHint: 'このファイルが対象とするプリンター向けに調整された値です。お使いのプリンターでは不適切または範囲外になる場合があります。',
     enqueuing: 'スライスジョブを送信中…',
     queued: '待機中…',
     failed: 'スライスに失敗。サイドカーのログを確認してください。',

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

@@ -4070,6 +4070,13 @@ export default {
     },
   },
   slicerSettings: {
+    filamentDefault: '기본값',
+    fromFile: '파일에서',
+    fromFileHint: '디자이너가 원본 파일에서 이 항목을 변경했습니다. 값은 {{value}}입니다.',
+    fromFilePrinterCoupled: '디자이너의 프린터',
+    fromFilePrinterCoupledHint: '이 파일이 설계된 프린터에 맞춘 값입니다. 사용 중인 프린터에서는 잘못되거나 범위를 벗어날 수 있습니다.',
+    useFromFile: '{{option}}에 원본 파일의 값 사용',
+    otherFromFile: '이 파일의 기타 설정',
     loading: '슬라이서 설정을 불러오는 중…',
     mode: {
       simple: '간단',
@@ -4082,6 +4089,12 @@ export default {
     noMatches: '검색과 일치하는 설정이 없습니다.',
   },
   slice: {
+    filamentSlotUnset: '미설정',
+    processSettingsEmbedded: "'파일에 포함된 설정 사용'이 켜져 있는 동안에는 사용되지 않습니다. 이 슬라이스는 파일 자체의 설정을 따릅니다.",
+    processSettingsInactive: '비활성',
+    presetsHidden: '{{count}}개 숨김',
+    showAllPresets: '모두 표시',
+    showFewerPresets: '간략히 표시',
     processSettings: '프로세스 설정',
     processSettingsHint: '이 슬라이스에 사용할 프리셋을 조정합니다. 건드리지 않은 항목은 프리셋 정의를 그대로 따릅니다.',
     processSettingsChanged: '{{count}}개 변경됨',
@@ -4111,11 +4124,6 @@ export default {
     autoOrientHint: '슬라이싱하기 전에 각 개체를 출력하기 좋은 면으로 회전시킵니다. 파일에 저장된 방향을 덮어씁니다.',
     autoArrange: '플레이트에 자동 배치',
     autoArrangeHint: '개체가 겹치지 않도록 슬라이서가 다시 배치합니다. 파일의 배치를 대체합니다.',
-    designSettings: '디자이너 설정 유지',
-    designSettingsHint: '이 파일은 기본 프로파일에서 {{count}}개의 출력 설정을 변경합니다.',
-    designSettingsSelected: '{{total}}개 중 {{selected}}개 선택됨',
-    designSettingsPrinterCoupled: '프린터 전용',
-    designSettingsPrinterCoupledHint: '이 파일이 대상으로 한 프린터에 맞춘 값입니다. 사용 중인 프린터에서는 잘못되었거나 범위를 벗어날 수 있습니다.',
     enqueuing: '슬라이싱 작업 제출 중…',
     queued: '대기 중…',
     failed: '슬라이싱 실패. 슬라이서 사이드카 로그를 확인하세요.',

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

@@ -4267,6 +4267,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'Padrão',
+    fromFile: 'do arquivo',
+    fromFileHint: 'O designer alterou isto no arquivo de origem. O valor é {{value}}.',
+    fromFilePrinterCoupled: 'impressora do designer',
+    fromFilePrinterCoupledHint: 'Ajustado para a impressora para a qual este arquivo foi projetado — pode estar errado ou fora de faixa na sua.',
+    useFromFile: 'Usar o valor do arquivo de origem para {{option}}',
+    otherFromFile: 'Outras configurações deste arquivo',
     loading: 'Carregando configurações do fatiador…',
     mode: {
       simple: 'Simples',
@@ -4279,6 +4286,12 @@ export default {
     noMatches: 'Nenhuma configuração corresponde a esta busca.',
   },
   slice: {
+    filamentSlotUnset: 'não definido',
+    processSettingsEmbedded: 'Não é usado enquanto “Usar as configurações internas do arquivo” estiver ativo — as configurações do próprio arquivo conduzem este fatiamento.',
+    processSettingsInactive: 'Inativo',
+    presetsHidden: '{{count}} ocultos',
+    showAllPresets: 'Mostrar todos',
+    showFewerPresets: 'Mostrar menos',
     processSettings: 'Configurações de processo',
     processSettingsHint: 'Ajuste o perfil escolhido para este fatiamento. Tudo o que você não alterar permanece como o perfil define.',
     processSettingsChanged: '{{count}} alterados',
@@ -4312,11 +4325,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'específico da impressora',
-    designSettingsPrinterCoupledHint: 'Ajustado para a impressora para a qual o arquivo foi projetado — pode estar incorreto ou fora de faixa na sua.',
     enqueuing: 'Enviando trabalho de fatiamento…',
     queued: 'Na fila…',
     failed: 'Falha ao fatiar. Verifique os logs do sidecar.',

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

@@ -4062,6 +4062,13 @@ export default {
     },
   },
   slicerSettings: {
+    filamentDefault: 'По умолчанию',
+    fromFile: 'из файла',
+    fromFileHint: 'Автор модели изменил этот параметр в исходном файле. Значение: {{value}}.',
+    fromFilePrinterCoupled: 'принтер автора',
+    fromFilePrinterCoupledHint: 'Подобрано под принтер, для которого создан файл, — на вашем значение может быть неверным или вне диапазона.',
+    useFromFile: 'Использовать значение из исходного файла для «{{option}}»',
+    otherFromFile: 'Другие параметры из этого файла',
     loading: 'Загрузка настроек слайсера…',
     mode: {
       simple: 'Простой',
@@ -4074,6 +4081,12 @@ export default {
     noMatches: 'Нет параметров, соответствующих запросу.',
   },
   slice: {
+    filamentSlotUnset: 'не задано',
+    processSettingsEmbedded: 'Не используется, пока включено «Использовать встроенные настройки файла» — нарезкой управляют настройки самого файла.',
+    processSettingsInactive: 'Не активно',
+    presetsHidden: 'скрыто: {{count}}',
+    showAllPresets: 'Показать все',
+    showFewerPresets: 'Показать меньше',
     processSettings: 'Параметры процесса',
     processSettingsHint: 'Настройте выбранный профиль для этой нарезки. Всё, что вы не измените, останется как задано в профиле.',
     processSettingsChanged: 'изменено: {{count}}',
@@ -4107,11 +4120,6 @@ export default {
     autoOrientHint: 'Слайсер повернёт каждый объект на сторону, которая печатается лучше всего. Ориентация из файла будет заменена.',
     autoArrange: 'Автоматически разместить на столе',
     autoArrangeHint: 'Слайсер расставит объекты так, чтобы они не перекрывались. Расположение из файла будет заменено.',
-    designSettings: "Сохранить настройки автора",
-    designSettingsHint: "Этот файл меняет {{count}} настроек печати по сравнению со стандартным профилем.",
-    designSettingsSelected: "Выбрано {{selected}} из {{total}}",
-    designSettingsPrinterCoupled: "зависит от принтера",
-    designSettingsPrinterCoupledHint: "Значение подобрано для принтера, под который создан файл, — на вашем оно может быть неверным или вне допустимого диапазона.",
     enqueuing: "Отправка задания на нарезку…",
     queued: "В очереди…",
     failed: "Ошибка нарезки. Проверьте журналы вспомогательного сервиса слайсера.",

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

@@ -4268,6 +4268,13 @@ export default {
 
   // Dilimle (SliceModal ile slicer-API entegrasyonu)
   slicerSettings: {
+    filamentDefault: 'Varsayılan',
+    fromFile: 'dosyadan',
+    fromFileHint: 'Tasarımcı bunu kaynak dosyada değiştirdi. Değeri {{value}}.',
+    fromFilePrinterCoupled: 'tasarımcının yazıcısı',
+    fromFilePrinterCoupledHint: 'Bu dosyanın tasarlandığı yazıcıya göre ayarlanmıştır; sizinkinde yanlış veya aralık dışı olabilir.',
+    useFromFile: '{{option}} için kaynak dosyadaki değeri kullan',
+    otherFromFile: 'Bu dosyadaki diğer ayarlar',
     loading: 'Dilimleyici ayarları yükleniyor…',
     mode: {
       simple: 'Basit',
@@ -4280,6 +4287,12 @@ export default {
     noMatches: 'Bu aramayla eşleşen ayar yok.',
   },
   slice: {
+    filamentSlotUnset: 'ayarlanmadı',
+    processSettingsEmbedded: '“Dosyanın yerleşik ayarlarını kullan” açıkken kullanılmaz — bu dilimlemeyi dosyanın kendi ayarları yönetir.',
+    processSettingsInactive: 'Etkin değil',
+    presetsHidden: '{{count}} gizli',
+    showAllPresets: 'Tümünü göster',
+    showFewerPresets: 'Daha az göster',
     processSettings: 'İşlem ayarları',
     processSettingsHint: 'Seçilen ön ayarı bu dilimleme için düzenleyin. Dokunmadığınız her şey ön ayardaki gibi kalır.',
     processSettingsChanged: '{{count}} değişti',
@@ -4313,11 +4326,6 @@ export default {
     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',
-    designSettingsPrinterCoupled: 'yazıcıya özel',
-    designSettingsPrinterCoupledHint: 'Dosyanın tasarlandığı yazıcıya göre ayarlanmış — sizinkinde yanlış veya aralık dışı olabilir.',
     enqueuing: 'Dilimleme işi gönderiliyor…',
     queued: 'Kuyrukta…',
     failed: 'Dilimleme başarısız. Dilimleyici yardımcı bileşen günlüklerini kontrol edin.',

+ 13 - 5
frontend/src/i18n/locales/uk.ts

@@ -4312,6 +4312,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: 'За замовчуванням',
+    fromFile: 'з файлу',
+    fromFileHint: 'Автор моделі змінив цей параметр у вихідному файлі. Значення: {{value}}.',
+    fromFilePrinterCoupled: 'принтер автора',
+    fromFilePrinterCoupledHint: 'Підібрано під принтер, для якого створено файл, — на вашому значення може бути хибним або поза діапазоном.',
+    useFromFile: 'Використовувати значення з вихідного файлу для «{{option}}»',
+    otherFromFile: 'Інші параметри з цього файлу',
     loading: 'Завантаження налаштувань слайсера…',
     mode: {
       simple: 'Простий',
@@ -4324,6 +4331,12 @@ export default {
     noMatches: 'Немає параметрів, що відповідають запиту.',
   },
   slice: {
+    filamentSlotUnset: 'не задано',
+    processSettingsEmbedded: 'Не використовується, доки увімкнено «Використовувати вбудовані параметри файлу» — нарізанням керують параметри самого файлу.',
+    processSettingsInactive: 'Неактивно',
+    presetsHidden: 'приховано: {{count}}',
+    showAllPresets: 'Показати всі',
+    showFewerPresets: 'Показати менше',
     processSettings: 'Параметри процесу',
     processSettingsHint: 'Налаштуйте вибраний профіль для цього нарізання. Усе, чого ви не змінили, лишається як визначено профілем.',
     processSettingsChanged: 'змінено: {{count}}',
@@ -4357,11 +4370,6 @@ export default {
     autoOrientHint: "Слайсер поверне кожен об'єкт на бік, який друкується найкраще. Орієнтацію з файлу буде замінено.",
     autoArrange: 'Автоматично розмістити на столі',
     autoArrangeHint: "Слайсер розставить об'єкти так, щоб вони не перекривалися. Розташування з файлу буде замінено.",
-    designSettings: "Зберегти налаштування автора",
-    designSettingsHint: "Цей файл змінює {{count}} налаштувань друку порівняно зі стандартним профілем.",
-    designSettingsSelected: "Вибрано {{selected}} із {{total}}",
-    designSettingsPrinterCoupled: "залежить від принтера",
-    designSettingsPrinterCoupledHint: "Значення підібрано для принтера, під який створено файл, — на вашому воно може бути неправильним або поза допустимим діапазоном.",
     enqueuing: "Надсилання завдання нарізання…",
     queued: "У черзі…",
     failed: "Не вдалося виконати нарізання. Перевірте журнали слайсера.",

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

@@ -4267,6 +4267,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: '默认',
+    fromFile: '来自文件',
+    fromFileHint: '设计者在源文件中修改了此项,其值为 {{value}}。',
+    fromFilePrinterCoupled: '设计者的打印机',
+    fromFilePrinterCoupledHint: '针对该文件设计时所用的打印机调校,在你的打印机上可能不正确或超出范围。',
+    useFromFile: '对 {{option}} 使用源文件中的值',
+    otherFromFile: '此文件中的其他设置',
     loading: '正在加载切片设置…',
     mode: {
       simple: '简单',
@@ -4279,6 +4286,12 @@ export default {
     noMatches: '没有与搜索匹配的设置。',
   },
   slice: {
+    filamentSlotUnset: '未设置',
+    processSettingsEmbedded: '启用“使用文件内置设置”时不生效——本次切片由文件自身的设置决定。',
+    processSettingsInactive: '未启用',
+    presetsHidden: '已隐藏 {{count}} 项',
+    showAllPresets: '显示全部',
+    showFewerPresets: '显示较少',
     processSettings: '工艺设置',
     processSettingsHint: '为本次切片调整所选预设。未改动的项目仍按预设定义。',
     processSettingsChanged: '已更改 {{count}} 项',
@@ -4312,11 +4325,6 @@ export default {
     autoOrientHint: '切片前由切片器把每个模型转到最适合打印的一面,会覆盖文件中保存的朝向。',
     autoArrange: '自动排布在热床上',
     autoArrangeHint: '由切片器重新摆放模型,使其不再重叠,会替换文件自带的布局。',
-    designSettings: '保留设计者的设置',
-    designSettingsHint: '此文件相对标准配置修改了 {{count}} 项打印设置。',
-    designSettingsSelected: '已选择 {{selected}} / {{total}}',
-    designSettingsPrinterCoupled: '与打印机相关',
-    designSettingsPrinterCoupledHint: '该值是为此文件面向的打印机调校的,在你的打印机上可能不正确或超出范围。',
     enqueuing: '提交切片任务中…',
     queued: '已排队…',
     failed: '切片失败。请检查切片器 sidecar 日志。',

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

@@ -4267,6 +4267,13 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    filamentDefault: '預設',
+    fromFile: '來自檔案',
+    fromFileHint: '設計者在來源檔案中修改了此項,其值為 {{value}}。',
+    fromFilePrinterCoupled: '設計者的印表機',
+    fromFilePrinterCoupledHint: '針對該檔案設計時所用的印表機調校,在你的印表機上可能不正確或超出範圍。',
+    useFromFile: '對 {{option}} 使用來源檔案中的值',
+    otherFromFile: '此檔案中的其他設定',
     loading: '正在載入切片設定…',
     mode: {
       simple: '簡易',
@@ -4279,6 +4286,12 @@ export default {
     noMatches: '沒有符合搜尋的設定。',
   },
   slice: {
+    filamentSlotUnset: '未設定',
+    processSettingsEmbedded: '啟用「使用檔案內建設定」時不生效——本次切片由檔案自身的設定決定。',
+    processSettingsInactive: '未啟用',
+    presetsHidden: '已隱藏 {{count}} 項',
+    showAllPresets: '顯示全部',
+    showFewerPresets: '顯示較少',
     processSettings: '列印參數',
     processSettingsHint: '為本次切片調整所選預設。未變更的項目仍依預設定義。',
     processSettingsChanged: '已變更 {{count}} 項',
@@ -4312,11 +4325,6 @@ export default {
     autoOrientHint: '切片前由切片器將每個模型轉到最適合列印的一面,會覆蓋檔案中儲存的朝向。',
     autoArrange: '自動排列在熱床上',
     autoArrangeHint: '由切片器重新擺放模型,使其不再重疊,會取代檔案自帶的版面配置。',
-    designSettings: '保留設計者的設定',
-    designSettingsHint: '此檔案相對標準設定檔修改了 {{count}} 項列印設定。',
-    designSettingsSelected: '已選擇 {{selected}} / {{total}}',
-    designSettingsPrinterCoupled: '與印表機相關',
-    designSettingsPrinterCoupledHint: '此值是為該檔案面向的印表機調校的,在你的印表機上可能不正確或超出範圍。',
     enqueuing: '提交切片任務中…',
     queued: '已排隊…',
     failed: '切片失敗。請檢查切片器 sidecar 日誌。',

+ 9 - 8
frontend/src/lib/slicerSettings.ts

@@ -18,8 +18,10 @@ const VECTOR_TYPES = new Set(['coBools', 'coFloats', 'coFloatsOrPercents']);
 export const isVectorOption = (option: ProcessOption): boolean => VECTOR_TYPES.has(option.type);
 
 /**
- * Numeric bound from the schema, or `undefined` when the extractor left an
- * unparsed C++ literal behind (`"0.3f"`, `"def_infill_anchor_min->sidetext"`).
+ * Numeric bound from the schema, or `undefined` when it isn't a number at all.
+ * Float literals are normalised by the generator, but a handful of bounds are
+ * unresolved C++ expressions the extractor could not follow, and those must not
+ * reach an input's `min`/`max`.
  */
 export function numericBound(bound: number | string | undefined): number | undefined {
   if (typeof bound === 'number') return Number.isFinite(bound) ? bound : undefined;
@@ -43,11 +45,10 @@ export function displaySidetext(option: ProcessOption): string | undefined {
 export function defaultForDisplay(option: ProcessOption): string {
   const d = option.default;
   if (d === undefined) return '';
-  if (Array.isArray(d)) {
-    // A handful of defaults were mis-extracted from C++ float literals —
-    // `0.f` became [0, "f"]. Drop the stray suffix token rather than render it.
-    return d.filter((v) => v !== 'f').map(String).join(', ');
-  }
+  // Per-extruder vectors render as a comma-separated list. C++ literal
+  // artefacts (`0.`, `0.3f`, `100.%`) are normalised by
+  // scripts/generate-slicer-schema.mjs, so nothing needs unpicking here.
+  if (Array.isArray(d)) return d.map(String).join(', ');
   if (typeof d === 'boolean') return d ? '1' : '0';
   return String(d);
 }
@@ -106,7 +107,7 @@ export function isModified(option: ProcessOption, value: SettingValue | undefine
   const d = option.default;
   if (d === undefined) return asString !== '';
 
-  const defaultSerialized = serializeSetting(option, Array.isArray(d) ? d.filter((v) => v !== 'f').map(String).join(', ') : (d as SettingValue));
+  const defaultSerialized = serializeSetting(option, Array.isArray(d) ? d.map(String).join(', ') : (d as SettingValue));
   const defaultString = Array.isArray(defaultSerialized) ? defaultSerialized.join(', ') : defaultSerialized;
 
   return asString !== defaultString;

+ 3 - 3
frontend/src/types/slicerSettings.ts

@@ -37,9 +37,9 @@ export interface ProcessOption {
   /** Unit shown after the input ("mm", "mm/s²", "%"). */
   sidetext?: string;
   /**
-   * Bounds as the extractor found them. Usually numeric, but a few carry
-   * unparsed C++ float literals ("0.3f") — callers must coerce and ignore what
-   * doesn't convert.
+   * Bounds, normalised out of their C++ source form by the generator. Typed as
+   * string too because the JSON carries both shapes; coerce with
+   * ``numericBound`` rather than assuming a number.
    */
   min?: number | string;
   max?: number | string;

+ 36 - 5
frontend/src/utils/slicerPrinterMatch.ts

@@ -13,9 +13,12 @@
 //      derived from the backend's canonical PRINTER_MODEL_MAP (fetched via
 //      /slicer/printer-models), not duplicated here.
 //
-// The result drives grouping, not hard hiding: a preset no rule covers
-// stays in the main list, and only a preset that resolves to a *different*
-// printer is pushed into an "Other printers" group.
+// Only a definite 'mismatch' is acted on: the dropdown holds those back
+// behind a "Show all" link. A preset no rule covers classifies as 'unknown'
+// and always stays in the list — absence of evidence is not evidence of
+// incompatibility, and hiding an untagged preset would make a user's own
+// imported profiles disappear. That asymmetry is why every parse failure
+// below returns 'unknown' rather than guessing a mismatch.
 
 export type PrinterCompatibility = 'match' | 'mismatch' | 'unknown';
 
@@ -100,6 +103,29 @@ function normalizeModelFragment(s: string): string {
   return s.replace(/\s+/g, '').toLowerCase();
 }
 
+/**
+ * Drop BambuStudio's ``"# "`` user-clone prefix.
+ *
+ * Editing a system preset saves a copy named ``"# Bambu Lab X1 Carbon 0.4
+ * nozzle"``, and `.bbscfg` bundle exports use the same convention. Two places
+ * need it off:
+ *
+ *   1. ``extractPrinterPresetModel`` — the prefix fails its "Bambu Lab …"
+ *      test, so a cloned *printer* made every preset classify as 'unknown'
+ *      and the dropdown filter silently did nothing.
+ *   2. The ``compatible_printers`` comparison, where a prefix on one side
+ *      alone reads as a mismatch against the very printer the preset was
+ *      cloned from — and a mismatch now hides the preset.
+ *
+ * The ``@`` tag extractors need no such handling: they scan for "@BBL " or
+ * the last "@", both of which skip a leading prefix already.
+ *
+ * The backend normalises the same prefix in ``_canonical_printer_model``.
+ */
+function stripUserClonePrefix(name: string): string {
+  return name.replace(/^#\s*/, '').trim();
+}
+
 // Bambu Studio's naming convention for bundled presets: the 0.4 nozzle is
 // the default and its variants drop the nozzle suffix; 0.2 / 0.6 / 0.8
 // carry an explicit "<size> nozzle" segment. So a process with no suffix
@@ -132,7 +158,7 @@ function extractBblToken(presetName: string): { token: string; nozzle: string |
 // nozzle]" printer preset name. Returns null for non-Bambu printer
 // presets — there is no reliable name-based match against those.
 function extractPrinterPresetModel(printerPresetName: string): { model: string; nozzle: string | null } | null {
-  const m = printerPresetName.match(/^Bambu Lab\s+(.+)$/i);
+  const m = stripUserClonePrefix(printerPresetName).match(/^Bambu Lab\s+(.+)$/i);
   if (!m) return null;
   const { stripped, nozzle } = takeNozzleSuffix(m[1]);
   return stripped ? { model: stripped, nozzle } : null;
@@ -282,7 +308,12 @@ export function presetCompatibility(
   // authoritative when set.
   const compat = preset.compatible_printers;
   if (compat && compat.length > 0) {
-    return compat.includes(selectedPrinterName) ? 'match' : 'mismatch';
+    // Compared with the clone prefix off both sides: a preset cloned from a
+    // system printer lists the *unprefixed* name, and comparing that raw
+    // against a selected "# Bambu Lab …" reads as a mismatch — which now
+    // hides the preset rather than merely demoting it.
+    const selected = stripUserClonePrefix(selectedPrinterName);
+    return compat.some((name) => stripUserClonePrefix(name) === selected) ? 'match' : 'mismatch';
   }
   // (2) BambuStudio's `@BBL <model>` name convention — covers cloud /
   // standard presets that don't carry compatible_printers.

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
static/assets/index-D4VkH83v.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
static/assets/index-DGpcnZPX.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
static/assets/index-DcBH50JZ.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 4 - 4
static/assets/process-schema-CzfeynAH.js


+ 2 - 2
static/index.html

@@ -26,8 +26,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-D5dXOgnd.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-DcBH50JZ.css">
+    <script type="module" crossorigin src="/assets/index-DGpcnZPX.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-D4VkH83v.css">
   </head>
   <body>
     <div id="root"></div>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.