Merge branch 'dev' into feature/ukrainian-localization

This commit is contained in:
MartinNYHC
2026-07-28 15:13:34 +02:00
committed by GitHub
27 changed files with 9847 additions and 6 deletions
@@ -332,6 +332,138 @@ describe('SliceModal', () => {
expect(screen.queryByLabelText(/Use the file's built-in settings/)).toBeNull();
});
// #2622: a MakerWorld 3MF designed for another printer carries the author's
// own process tweaks. BambuStudio records which keys deviate from the stock
// preset inside the file, so a cross-printer re-slice can carry them instead
// of losing them to the picked process profile.
const designedFor = {
file_id: 100,
filename: 'Designed.3mf',
plates: [],
is_multi_plate: false,
embedded_printer: 'Bambu Lab A1 0.4 nozzle',
embedded_process: '0.20mm Standard @BBL A1',
design_overrides: [
{ key: 'wall_loops', value: '5', printer_coupled: false },
{ key: 'sparse_infill_density', value: '100%', printer_coupled: false },
{ key: 'outer_wall_speed', value: '200', printer_coupled: true },
],
};
async function openDesignSection() {
const user = userEvent.setup();
await user.click(await screen.findByText(/Keep the designer's settings/));
return user;
}
it("carries the design's printer-independent settings by default (#2622)", async () => {
mockApi.sliceLibraryFile.mockResolvedValue({
job_id: 42,
status: 'pending',
status_url: '/api/v1/slice-jobs/42',
});
mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
onClose: vi.fn(),
});
// Two of three pre-selected: the speed key is machine-coupled.
expect(await screen.findByText('2 of 3 selected')).toBeInTheDocument();
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: /^Slice$/ }));
await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as { design_overrides?: string[] };
expect([...(payload.design_overrides ?? [])].sort()).toEqual(['sparse_infill_density', 'wall_loops']);
});
it('lists every changed setting with its value and flags the machine-coupled ones (#2622)', async () => {
mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
onClose: vi.fn(),
});
await openDesignSection();
expect(screen.getByText('wall_loops')).toBeInTheDocument();
expect(screen.getByText('5')).toBeInTheDocument();
expect(screen.getByText('sparse_infill_density')).toBeInTheDocument();
expect(screen.getByText('100%')).toBeInTheDocument();
// The risky one is listed too — visible, explained, just not pre-ticked.
expect(screen.getByText('outer_wall_speed')).toBeInTheDocument();
expect(screen.getByText('printer-specific')).toBeInTheDocument();
});
it('lets the user opt a machine-coupled setting in and a safe one out (#2622)', async () => {
mockApi.sliceLibraryFile.mockResolvedValue({
job_id: 42,
status: 'pending',
status_url: '/api/v1/slice-jobs/42',
});
mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
onClose: vi.fn(),
});
const user = await openDesignSection();
const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
const byKey = (key: string) =>
boxes.find((b) => b.closest('label')?.textContent?.includes(key)) as HTMLInputElement;
await user.click(byKey('outer_wall_speed'));
await user.click(byKey('wall_loops'));
await user.click(screen.getByRole('button', { name: /^Slice$/ }));
await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
const payload = mockApi.sliceLibraryFile.mock.calls[0][1] as { design_overrides?: string[] };
expect([...(payload.design_overrides ?? [])].sort()).toEqual(['outer_wall_speed', 'sparse_infill_density']);
});
it('omits design_overrides entirely when the user unticks everything (#2622)', async () => {
mockApi.sliceLibraryFile.mockResolvedValue({
job_id: 42,
status: 'pending',
status_url: '/api/v1/slice-jobs/42',
});
mockApi.getLibraryFilePlates.mockResolvedValue(designedFor);
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
onClose: vi.fn(),
});
const user = await openDesignSection();
const boxes = screen.getAllByRole('checkbox') as HTMLInputElement[];
for (const box of boxes) {
if (box.checked) await user.click(box);
}
await user.click(screen.getByRole('button', { name: /^Slice$/ }));
await waitFor(() => expect(mockApi.sliceLibraryFile).toHaveBeenCalled());
expect(mockApi.sliceLibraryFile.mock.calls[0][1]).not.toHaveProperty('design_overrides');
});
it('hides the section for a file that changes nothing (#2622)', async () => {
mockApi.getLibraryFilePlates.mockResolvedValue({ ...designedFor, design_overrides: [] });
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Designed.3mf' },
onClose: vi.fn(),
});
await waitFor(() => expect(screen.getByRole('button', { name: /^Slice$/ })).toBeEnabled());
expect(screen.queryByText(/Keep the designer's settings/)).toBeNull();
});
it('includes bed_type in the request when the user picks a non-auto plate (#1337)', async () => {
const onClose = vi.fn();
mockApi.sliceLibraryFile.mockResolvedValue({
+96 -1
View File
@@ -16,7 +16,7 @@ import {
import { useSliceJobTracker } from '../contexts/SliceJobTrackerContext';
import { useToast } from '../contexts/ToastContext';
import { PlatePickerModal } from './PlatePickerModal';
import type { PlateFilament } from '../types/plates';
import type { DesignOverride, PlateFilament } from '../types/plates';
import {
presetCompatibility,
buildCompatibilityIndex,
@@ -186,6 +186,16 @@ function formatElapsed(seconds: number): string {
return `${h}h ${remM}m`;
}
// Render a slicer parameter value for the design-settings list. Bambu's process
// schema stores everything as strings or arrays of strings, so this only has to
// flatten arrays and keep scalars readable — no unit or type interpretation,
// which would rot against every slicer release.
function formatDesignValue(value: unknown): string {
if (Array.isArray(value)) return value.map((v) => String(v)).join(', ');
if (value == null) return '';
return String(value);
}
export function SliceModal({ source, onClose }: SliceModalProps) {
const { t } = useTranslation();
const { trackJob } = useSliceJobTracker();
@@ -227,6 +237,15 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
// see canUseEmbedded below.
const [useEmbedded, setUseEmbedded] = useState(false);
// #2622: process settings the designer changed away from the stock preset,
// carried onto the picked process profile so a cross-printer re-slice keeps
// the model's intended wall count / infill / first layer instead of losing
// them to --load-settings. Keys the file flags as machine-coupled (speeds,
// accelerations, prime-tower geometry) are listed but start unticked — those
// were tuned for the designer's printer and can be plain wrong on another.
const [designKeys, setDesignKeys] = useState<Set<string>>(new Set());
const [designExpanded, setDesignExpanded] = useState(false);
// Slicer Pipelines (#1425) — apply a saved preset bundle to all four slots
// with one pick, or save the current selection as a new pipeline.
const pipelinesQuery = useQuery({
@@ -385,6 +404,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
// plates query resolves before the presets query (the latter is gated on
// it), so these are known by the time the pre-pick effects run.
const embeddedPrinter = platesQuery.data?.embedded_printer ?? null;
const designOverrides = useMemo<DesignOverride[]>(
() => platesQuery.data?.design_overrides ?? [],
[platesQuery.data],
);
const embeddedProcess = platesQuery.data?.embedded_process ?? null;
// "Slice as designed" is offered only when the source carries embedded
@@ -405,6 +428,12 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
if (!canUseEmbedded) setUseEmbedded(false);
}, [canUseEmbedded]);
// Pre-tick the printer-independent design settings once the source's list
// arrives. Machine-coupled keys stay off until the user opts in explicitly.
useEffect(() => {
setDesignKeys(new Set(designOverrides.filter((o) => !o.printer_coupled).map((o) => o.key)));
}, [designOverrides]);
// Printer pre-pick: defaults to the printer the 3MF was prepared for when
// that preset is available, else the first listed printer. Runs once when
// presets first arrive; later re-renders preserve any manual choice.
@@ -505,6 +534,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
// them) but go unused when this flag is set — the slicer falls back on
// the file's embedded project_settings.config instead.
...(useEmbedded && canUseEmbedded ? { use_embedded_settings: true } : {}),
// Carried design settings are patched onto the resolved process JSON,
// which the embedded-settings path never sends — so they are mutually
// exclusive by construction (#2622).
...(!useEmbedded && designKeys.size > 0 ? { design_overrides: [...designKeys] } : {}),
};
}
@@ -775,6 +808,68 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
selectedPrinterName={selectedPrinterName}
compatIndex={compatIndex}
/>
{/* Designer's process tweaks (#2622). BambuStudio records which
keys deviate from the stock preset in the 3MF itself, so a
re-slice for another printer can carry them instead of
flattening them under --load-settings. Hidden entirely when
the source lists none, and disabled in embedded mode where
the process JSON these patch is never sent. */}
{designOverrides.length > 0 && (
<div className="rounded-lg border border-bambu-dark-tertiary bg-bambu-dark/40 p-3">
<button
type="button"
onClick={() => setDesignExpanded((v) => !v)}
className="flex w-full items-center justify-between gap-2 text-left"
>
<span className="text-sm text-white">
{t('slice.designSettings')}
<span className="block text-xs text-bambu-gray/70">
{t('slice.designSettingsHint', { count: designOverrides.length })}
</span>
</span>
<span className="shrink-0 text-xs text-bambu-gray">
{t('slice.designSettingsSelected', { selected: designKeys.size, total: designOverrides.length })}
</span>
</button>
{designExpanded && (
<div className="mt-3 space-y-1.5 border-t border-bambu-dark-tertiary pt-3">
{designOverrides.map((o) => (
<label
key={o.key}
className={`flex items-start gap-2 text-xs ${useEmbedded ? 'opacity-50' : 'cursor-pointer'}`}
>
<input
type="checkbox"
checked={designKeys.has(o.key)}
disabled={isEnqueuing || useEmbedded}
onChange={(e) => {
setDesignKeys((prev) => {
const next = new Set(prev);
if (e.target.checked) next.add(o.key);
else next.delete(o.key);
return next;
});
}}
className="mt-0.5 shrink-0 cursor-pointer"
/>
<span className="min-w-0 flex-1">
<span className="font-mono text-bambu-gray">{o.key}</span>
<span className="ml-1.5 break-all text-white">{formatDesignValue(o.value)}</span>
{o.printer_coupled && (
<span
className="ml-1.5 rounded bg-amber-100 px-1 py-0.5 text-[10px] text-amber-700 dark:bg-amber-500/20 dark:text-amber-400"
title={t('slice.designSettingsPrinterCoupledHint')}
>
{t('slice.designSettingsPrinterCoupled')}
</span>
)}
</span>
</label>
))}
</div>
)}
</div>
)}
{/* Bed-type override (#1337). Always visible, always enabled.
The backend patches curr_bed_type on the resolved process
JSON before forwarding to the sidecar. */}
+5
View File
@@ -4072,6 +4072,11 @@ export default {
allPresetsRequired: 'Alle Profile müssen ausgewählt sein',
useEmbedded: 'Eingebettete Einstellungen der Datei verwenden',
useEmbeddedHint: 'So slicen, wie der Ersteller es angelegt hat (Wände, Füllung, Filament), statt mit den obigen Profilen. Verfügbar, weil dein Drucker zur Datei passt.',
designSettings: 'Einstellungen des Erstellers behalten',
designSettingsHint: 'Diese Datei ändert {{count}} Druckeinstellung(en) gegenüber dem Standardprofil.',
designSettingsSelected: '{{selected}} von {{total}} ausgewählt',
designSettingsPrinterCoupled: 'druckerspezifisch',
designSettingsPrinterCoupledHint: 'Auf den Drucker abgestimmt, für den die Datei erstellt wurde — auf deinem kann der Wert falsch oder unzulässig sein.',
enqueuing: 'Slice-Auftrag wird übermittelt…',
queued: 'In Warteschlange…',
failed: 'Slicen fehlgeschlagen. Logs des Slicer-Sidecars prüfen.',
+5
View File
@@ -4106,6 +4106,11 @@ export default {
allPresetsRequired: 'All presets must be selected',
useEmbedded: "Use the file's built-in settings",
useEmbeddedHint: "Slice it the way the designer set it up (walls, infill, filament) instead of the profiles above. Offered because your printer matches the file's.",
designSettings: 'Keep the designer\'s settings',
designSettingsHint: 'This file changes {{count}} print setting(s) from the stock profile.',
designSettingsSelected: '{{selected}} of {{total}} selected',
designSettingsPrinterCoupled: 'printer-specific',
designSettingsPrinterCoupledHint: 'Tuned for the printer this file was designed for — may be wrong or out of range on yours.',
enqueuing: 'Submitting slice job…',
queued: 'Queued…',
failed: 'Slicing failed. Check the slicer sidecar logs.',
+5
View File
@@ -4075,6 +4075,11 @@ export default {
allPresetsRequired: 'Deben seleccionarse todos los preajustes',
useEmbedded: 'Usar la configuración incorporada del archivo',
useEmbeddedHint: 'Laminar tal como lo configuró el diseñador (perímetros, relleno, filamento) en lugar de los perfiles de arriba. Disponible porque tu impresora coincide con la del archivo.',
designSettings: 'Mantener los ajustes del diseñador',
designSettingsHint: 'Este archivo cambia {{count}} ajuste(s) de impresión respecto al perfil estándar.',
designSettingsSelected: '{{selected}} de {{total}} seleccionados',
designSettingsPrinterCoupled: 'específico de la impresora',
designSettingsPrinterCoupledHint: 'Ajustado para la impresora para la que se diseñó el archivo: puede ser incorrecto o quedar fuera de rango en la tuya.',
enqueuing: 'Enviando el trabajo de laminado…',
queued: 'En cola…',
failed: 'Error al laminar. Consulte los registros del contenedor auxiliar del laminador.',
+5
View File
@@ -4061,6 +4061,11 @@ export default {
allPresetsRequired: 'Tous les préréglages doivent être sélectionnés',
useEmbedded: 'Utiliser les réglages intégrés du fichier',
useEmbeddedHint: "Slicer tel que le concepteur l'a configuré (parois, remplissage, filament) au lieu des profils ci-dessus. Proposé car votre imprimante correspond à celle du fichier.",
designSettings: 'Conserver les réglages du concepteur',
designSettingsHint: 'Ce fichier modifie {{count}} réglage(s) d\'impression par rapport au profil standard.',
designSettingsSelected: '{{selected}} sur {{total}} sélectionnés',
designSettingsPrinterCoupled: 'spécifique à l\'imprimante',
designSettingsPrinterCoupledHint: 'Réglé pour l\'imprimante visée par le fichier — peut être incorrect ou hors plage sur la vôtre.',
enqueuing: 'Envoi du travail de découpage…',
queued: 'En file d\'attente…',
failed: 'Échec du découpage. Vérifiez les journaux du sidecar.',
+5
View File
@@ -4060,6 +4060,11 @@ export default {
allPresetsRequired: 'Tutti i preset devono essere selezionati',
useEmbedded: 'Usa le impostazioni integrate del file',
useEmbeddedHint: 'Slicia come impostato dal designer (pareti, riempimento, filamento) invece dei profili sopra. Disponibile perché la tua stampante corrisponde a quella del file.',
designSettings: 'Mantieni le impostazioni del progettista',
designSettingsHint: 'Questo file modifica {{count}} impostazione/i di stampa rispetto al profilo standard.',
designSettingsSelected: '{{selected}} di {{total}} selezionate',
designSettingsPrinterCoupled: 'specifico della stampante',
designSettingsPrinterCoupledHint: 'Tarato per la stampante per cui è stato progettato il file: sulla tua può essere errato o fuori intervallo.',
enqueuing: 'Invio lavoro di slicing…',
queued: 'In coda…',
failed: 'Slicing fallito. Controlla i log del sidecar.',
+5
View File
@@ -4072,6 +4072,11 @@ export default {
allPresetsRequired: 'すべてのプリセットを選択する必要があります',
useEmbedded: 'ファイルに埋め込まれた設定を使用',
useEmbeddedHint: '上のプロファイルではなく、設計者が設定したとおり(ウォール、インフィル、フィラメント)にスライスします。お使いのプリンターがファイルと一致するため利用できます。',
designSettings: '設計者の設定を保持',
designSettingsHint: 'このファイルは標準プロファイルから {{count}} 個の印刷設定を変更しています。',
designSettingsSelected: '{{total}} 個中 {{selected}} 個を選択',
designSettingsPrinterCoupled: 'プリンター固有',
designSettingsPrinterCoupledHint: 'このファイルが対象とするプリンター向けに調整された値です。お使いのプリンターでは不適切または範囲外になる場合があります。',
enqueuing: 'スライスジョブを送信中…',
queued: '待機中…',
failed: 'スライスに失敗。サイドカーのログを確認してください。',
+5
View File
@@ -3861,6 +3861,11 @@ export default {
allPresetsRequired: '모든 프리셋을 선택해야 합니다',
useEmbedded: '파일에 포함된 설정 사용',
useEmbeddedHint: '위 프로필 대신 디자이너가 설정한 대로(벽, 내부 채움, 필라멘트) 슬라이싱합니다. 프린터가 파일과 일치하여 사용할 수 있습니다.',
designSettings: '디자이너 설정 유지',
designSettingsHint: '이 파일은 기본 프로파일에서 {{count}}개의 출력 설정을 변경합니다.',
designSettingsSelected: '{{total}}개 중 {{selected}}개 선택됨',
designSettingsPrinterCoupled: '프린터 전용',
designSettingsPrinterCoupledHint: '이 파일이 대상으로 한 프린터에 맞춘 값입니다. 사용 중인 프린터에서는 잘못되었거나 범위를 벗어날 수 있습니다.',
enqueuing: '슬라이싱 작업 제출 중…',
queued: '대기 중…',
failed: '슬라이싱 실패. 슬라이서 사이드카 로그를 확인하세요.',
+5
View File
@@ -4060,6 +4060,11 @@ export default {
allPresetsRequired: 'Todas as predefinições devem ser selecionadas',
useEmbedded: 'Usar as configurações incorporadas do arquivo',
useEmbeddedHint: 'Fatiar como o designer configurou (paredes, preenchimento, filamento) em vez dos perfis acima. Disponível porque sua impressora corresponde à do arquivo.',
designSettings: 'Manter as configurações do designer',
designSettingsHint: 'Este arquivo altera {{count}} configuração(ões) de impressão em relação ao perfil padrão.',
designSettingsSelected: '{{selected}} de {{total}} selecionadas',
designSettingsPrinterCoupled: 'específico da impressora',
designSettingsPrinterCoupledHint: 'Ajustado para a impressora para a qual o arquivo foi projetado — pode estar incorreto ou fora de faixa na sua.',
enqueuing: 'Enviando trabalho de fatiamento…',
queued: 'Na fila…',
failed: 'Falha ao fatiar. Verifique os logs do sidecar.',
+5
View File
@@ -3857,6 +3857,11 @@ export default {
allPresetsRequired: "Необходимо выбрать все профили",
useEmbedded: "Использовать встроенные настройки файла",
useEmbeddedHint: "Нарезать так, как задумал автор модели (стенки, заполнение, филамент), вместо профилей выше. Доступно, потому что ваш принтер совпадает с указанным в файле.",
designSettings: "Сохранить настройки автора",
designSettingsHint: "Этот файл меняет {{count}} настроек печати по сравнению со стандартным профилем.",
designSettingsSelected: "Выбрано {{selected}} из {{total}}",
designSettingsPrinterCoupled: "зависит от принтера",
designSettingsPrinterCoupledHint: "Значение подобрано для принтера, под который создан файл, — на вашем оно может быть неверным или вне допустимого диапазона.",
enqueuing: "Отправка задания на нарезку…",
queued: "В очереди…",
failed: "Ошибка нарезки. Проверьте журналы вспомогательного сервиса слайсера.",
+5
View File
@@ -4062,6 +4062,11 @@ export default {
allPresetsRequired: 'Tüm ön ayarlar seçilmelidir',
useEmbedded: 'Dosyanın yerleşik ayarlarını kullan',
useEmbeddedHint: 'Yukarıdaki profiller yerine tasarımcının ayarladığı gibi (duvarlar, dolgu, filament) dilimle. Yazıcınız dosyayla eşleştiği için sunuluyor.',
designSettings: 'Tasarımcının ayarlarını koru',
designSettingsHint: 'Bu dosya standart profile göre {{count}} baskı ayarını değiştiriyor.',
designSettingsSelected: '{{total}} ayardan {{selected}} tanesi seçili',
designSettingsPrinterCoupled: 'yazıcıya özel',
designSettingsPrinterCoupledHint: 'Dosyanın tasarlandığı yazıcıya göre ayarlanmış — sizinkinde yanlış veya aralık dışı olabilir.',
enqueuing: 'Dilimleme işi gönderiliyor…',
queued: 'Kuyrukta…',
failed: 'Dilimleme başarısız. Dilimleyici yardımcı bileşen günlüklerini kontrol edin.',
+5
View File
@@ -4060,6 +4060,11 @@ export default {
allPresetsRequired: '必须选择所有预设',
useEmbedded: '使用文件的内置设置',
useEmbeddedHint: '按设计者的设置(壁、填充、耗材)切片,而非上方的配置文件。因您的打印机与文件匹配而可用。',
designSettings: '保留设计者的设置',
designSettingsHint: '此文件相对标准配置修改了 {{count}} 项打印设置。',
designSettingsSelected: '已选择 {{selected}} / {{total}}',
designSettingsPrinterCoupled: '与打印机相关',
designSettingsPrinterCoupledHint: '该值是为此文件面向的打印机调校的,在你的打印机上可能不正确或超出范围。',
enqueuing: '提交切片任务中…',
queued: '已排队…',
failed: '切片失败。请检查切片器 sidecar 日志。',
+5
View File
@@ -4060,6 +4060,11 @@ export default {
allPresetsRequired: '必須選擇所有預設',
useEmbedded: '使用檔案的內建設定',
useEmbeddedHint: '依設計者的設定(外牆、填充、耗材)切片,而非上方的設定檔。因您的印表機與檔案相符而可用。',
designSettings: '保留設計者的設定',
designSettingsHint: '此檔案相對標準設定檔修改了 {{count}} 項列印設定。',
designSettingsSelected: '已選擇 {{selected}} / {{total}}',
designSettingsPrinterCoupled: '與印表機相關',
designSettingsPrinterCoupledHint: '此值是為該檔案面向的印表機調校的,在你的印表機上可能不正確或超出範圍。',
enqueuing: '提交切片任務中…',
queued: '已排隊…',
failed: '切片失敗。請檢查切片器 sidecar 日誌。',
+15
View File
@@ -38,6 +38,21 @@ export interface PlateMetadata {
interface EmbeddedPresets {
embedded_printer?: string | null;
embedded_process?: string | null;
// Process settings the designer changed away from the stock preset, read
// from the 3MF's own `different_settings_to_system` (#2622). Offered in the
// SliceModal so a re-slice for another printer can carry them instead of
// losing them to the picked process profile. Empty for STL, OrcaSlicer
// files, and older exports that predate the field.
design_overrides?: DesignOverride[];
}
// One process setting the designer deviated on. `printer_coupled` marks the
// values that only make sense on the machine they were tuned for (speeds,
// accelerations, prime-tower geometry) — offered, but never pre-selected.
export interface DesignOverride {
key: string;
value: unknown;
printer_coupled: boolean;
}
export interface ArchivePlatesResponse extends EmbeddedPresets {