mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
fix(camera): take the finish photo when the print ends, not when its last layer starts (#2547)
The photo fired the moment layer_num reached total_layer_num. That edge is where the printer *starts* its final layer, not where it finishes it: the reporter's H2C capture shows it arriving at 92% with mc_remaining_time=2, three minutes and seventeen seconds and one filament change before the print actually ended, so the frame caught the toolhead mid-print over the model. The trigger also latched _finish_photo_captured, which locked out both the stage-22 and FINISH triggers for the rest of the print — so on firmware that never reports an end-of-print filament unload (H2C and A1 Mini confirmed) nothing could replace the bad frame. Remove the last-layer trigger. The photo is now taken at the FINISH-state trigger, which every model sends and which lands after the toolhead parks. Since Bambu's end G-code drops the plate ~100mm just before that, restore the framing before capturing: absolute G90/G1 Z to max_z_height + 10mm clearance, settle, capture, then drop it back so the print is as reachable as the printer left it. Absolute is the safety argument — that Z is a height the toolhead occupied seconds earlier, so it is inside the travel limits by construction and leaves the nozzle above the part, and it is unambiguous across model families because Z is the nozzle-to-bed gap whether the bed moves or the toolhead does. M211 is never touched (#2579). This is what #1145, #1397 and #1565 asked for. The height is only trusted when two independent sources agree: the archive is matched by the finished print's subtask_name by equality (not LIKE, so "Cube" cannot resolve to "Cube v2"), and its layer count from the 3MF must match the layer count the printer reported over MQTT. Matching on "most recent archive for this printer" was not safe — on_print_complete pops the _active_prints binding concurrently, and a print Bambuddy failed to archive would have resolved to its predecessor. A wrong height is the one failure that could drive the nozzle into the model. The move is additionally skipped when the print height is unknown, when a queue item is pending for the printer, when the printer has left FINISH, and when the new finish_photo_restore_plate setting is off. for every FINISH-state capture — which is what shipped the mid-print photo — the bank is used only when the dispatcher recorded that it injected End G-code into this print, since a SwapMod snippet may have ejected the plate. The flag is handed over in two steps (mark_pending at dispatch, adopt at print start) so it can never outlive its print: a job started from the slicer or SD card adopts False rather than inheriting its predecessor's answer. Those prints also skip the plate move outright, bank or no bank. The bank now refreshes on mc_percent advances as well as layer changes, via a new on_print_progress callback. Layer changes stop the instant the final layer begins, which left the #1867 fallback frame stale by the whole length of that layer; progress keeps ticking there and freezes before the End G-code runs, so a swapped plate still cannot reach the bank. The last-layer throttle exemption is dropped, since it would now fire a grab on every percent tick. On the timelapse path the moment producer returns early, so the consumer does the restore itself before its live-grab fallback — the documented usual outcome on P1-series, where the video has not transferred by the time the notification goes out and the shipped photo was of an already-dropped plate. The two waits are now derived from the settle window and the video poll timeout rather than hardcoded; at the old flat 75s that fallback was guaranteed to be cut off mid-settle. extract_max_z_height_from_3mf reads only a bounded prefix of the plate G-code, since a sliced plate is routinely tens of megabytes and the header is ~40 lines. It returns None for missing, unparseable, zero and negative values so callers must treat "don't know" as such rather than defaulting.
This commit is contained in:
@@ -110,6 +110,66 @@ describe('SettingsPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('finish photo plate restore (#2547)', () => {
|
||||
const restoreLabel = 'Restore plate for finish photo';
|
||||
|
||||
it('offers the toggle while finish photos are enabled', async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
expect(await screen.findByText(restoreLabel)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the toggle when finish photos are switched off', async () => {
|
||||
// It only describes how the finish photo is framed, so it is meaningless
|
||||
// when no finish photo is taken at all.
|
||||
server.use(
|
||||
http.get('/api/v1/settings/', () =>
|
||||
HttpResponse.json({ ...mockSettings, capture_finish_photo: false })
|
||||
)
|
||||
);
|
||||
render(<SettingsPage />);
|
||||
|
||||
await screen.findByRole('heading', { name: 'Settings' });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(restoreLabel)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to on when the backend has never stored the setting', async () => {
|
||||
// Existing installs have no row for it; the UI must not read that as off.
|
||||
render(<SettingsPage />);
|
||||
|
||||
const label = await screen.findByText(restoreLabel);
|
||||
const row = label.closest('div')!.parentElement!;
|
||||
expect(within(row).getByRole('checkbox')).toBeChecked();
|
||||
});
|
||||
|
||||
it('sends the new value on save', async () => {
|
||||
let saved: Record<string, unknown> | null = null;
|
||||
server.use(
|
||||
http.put('/api/v1/settings/', async ({ request }) => {
|
||||
saved = (await request.json()) as Record<string, unknown>;
|
||||
return HttpResponse.json({ ...mockSettings, ...saved });
|
||||
})
|
||||
);
|
||||
render(<SettingsPage />);
|
||||
|
||||
const label = await screen.findByText(restoreLabel);
|
||||
// The page suppresses auto-save for 100ms after the settings load, so a
|
||||
// click landing inside that window is swallowed with no re-trigger.
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
const row = label.closest('div')!.parentElement!;
|
||||
await userEvent.click(within(row).getByRole('checkbox'));
|
||||
|
||||
// The page auto-saves on a 500ms debounce, so the default 1s waitFor
|
||||
// window is only just wide enough — give the request room to land.
|
||||
await waitFor(() => {
|
||||
expect(saved).not.toBeNull();
|
||||
}, { timeout: 3000 });
|
||||
expect(saved!.finish_photo_restore_plate).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('general settings', () => {
|
||||
it('shows date format setting', async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
@@ -1185,6 +1185,7 @@ export interface AppSettings {
|
||||
auto_archive: boolean;
|
||||
save_thumbnails: boolean;
|
||||
capture_finish_photo: boolean;
|
||||
finish_photo_restore_plate: boolean;
|
||||
default_filament_cost: number;
|
||||
currency: string;
|
||||
energy_cost_per_kwh: number;
|
||||
|
||||
@@ -2446,6 +2446,8 @@ export default {
|
||||
autoArchiveDescription: '3MF-Dateien automatisch speichern, wenn Drucke abgeschlossen sind',
|
||||
saveThumbnailsDescription: 'Vorschaubilder aus 3MF-Dateien extrahieren und speichern',
|
||||
captureFinishPhotoDescription: 'Foto von der Druckerkamera aufnehmen, wenn der Druck abgeschlossen ist. Bambuddy zeichnet während des Drucks einen kurzen Zeitraffer auf, damit das Foto aus dem Moment vor dem Absenken der Druckplatte stammen kann. Die Zeitraffer-Datei bleibt erhalten, wenn du den Zeitraffer für diesen Druck aktiviert hast, andernfalls wird sie nach Aufnahme des Fotos automatisch gelöscht.',
|
||||
finishPhotoRestorePlate: 'Druckplatte für Abschlussfoto anheben',
|
||||
finishPhotoRestorePlateDescription: 'Der Drucker senkt die Druckplatte am Druckende um etwa 100 mm ab, wodurch der fertige Druck unterhalb des Kamerabildausschnitts liegt. Bambuddy hebt sie wieder bis knapp über die zuletzt gedruckte Schicht an, nimmt das Foto auf und senkt sie danach wieder ab. Wird übersprungen, wenn die Druckhöhe unbekannt ist oder ein weiterer Auftrag in der Warteschlange steht.',
|
||||
ffmpegNotInstalled: 'ffmpeg nicht installiert',
|
||||
ffmpegRequired: 'Kameraaufnahme benötigt ffmpeg. Installieren über <brew>brew install ffmpeg</brew> (macOS) oder <apt>apt install ffmpeg</apt> (Linux).',
|
||||
// Camera
|
||||
|
||||
@@ -2465,6 +2465,8 @@ export default {
|
||||
autoArchiveDescription: 'Automatically save 3MF files when prints complete',
|
||||
saveThumbnailsDescription: 'Extract and save preview images from 3MF files',
|
||||
captureFinishPhotoDescription: 'Take a photo from printer camera when print completes. Bambuddy records a brief timelapse during the print so the photo can be sourced from the moment before the bed drops; the timelapse file is kept if you enabled timelapse for this print, otherwise it is deleted automatically after the photo is captured.',
|
||||
finishPhotoRestorePlate: 'Restore plate for finish photo',
|
||||
finishPhotoRestorePlateDescription: 'The printer drops the build plate about 100 mm when a print ends, leaving the finished print below the camera\'s framing. Bambuddy raises it back to just above the last printed layer, takes the photo, then lowers it again. Skipped when the print height is unknown or another job is queued.',
|
||||
ffmpegNotInstalled: 'ffmpeg not installed',
|
||||
ffmpegRequired: 'Camera capture requires ffmpeg. Install it via <brew>brew install ffmpeg</brew> (macOS) or <apt>apt install ffmpeg</apt> (Linux).',
|
||||
// Camera
|
||||
|
||||
@@ -2449,6 +2449,8 @@ export default {
|
||||
autoArchiveDescription: 'Guardar automáticamente los archivos 3MF cuando se completan las impresiones',
|
||||
saveThumbnailsDescription: 'Extraer y guardar imágenes de vista previa de los archivos 3MF',
|
||||
captureFinishPhotoDescription: 'Tomar una foto desde la cámara de la impresora cuando se completa la impresión. Bambuddy graba un breve timelapse durante la impresión para que la foto pueda obtenerse del momento previo al descenso de la cama; el archivo del timelapse se conserva si activaste el timelapse para esta impresión, de lo contrario se elimina automáticamente tras capturar la foto.',
|
||||
finishPhotoRestorePlate: 'Elevar la cama para la foto final',
|
||||
finishPhotoRestorePlateDescription: 'La impresora baja la cama unos 100 mm al terminar una impresión, dejando la pieza terminada por debajo del encuadre de la cámara. Bambuddy la vuelve a subir hasta justo encima de la última capa impresa, toma la foto y luego la baja de nuevo. Se omite si se desconoce la altura de la impresión o si hay otro trabajo en cola.',
|
||||
ffmpegNotInstalled: 'ffmpeg no instalado',
|
||||
ffmpegRequired: 'La captura de cámara requiere ffmpeg. Instálelo mediante <brew>brew install ffmpeg</brew> (macOS) o <apt>apt install ffmpeg</apt> (Linux).',
|
||||
// Camera
|
||||
|
||||
@@ -2400,6 +2400,8 @@ export default {
|
||||
autoArchiveDescription: 'Sauvegarder automatiquement les fichiers 3MF à la fin des impressions',
|
||||
saveThumbnailsDescription: 'Extraire et sauvegarder les images d\'aperçu des fichiers 3MF',
|
||||
captureFinishPhotoDescription: 'Prendre une photo avec la caméra de l\'imprimante à la fin de l\'impression. Bambuddy enregistre un court timelapse pendant l\'impression afin que la photo puisse provenir du moment précédant l\'abaissement du plateau ; le fichier du timelapse est conservé si vous avez activé le timelapse pour cette impression, sinon il est supprimé automatiquement après la capture de la photo.',
|
||||
finishPhotoRestorePlate: 'Remonter le plateau pour la photo finale',
|
||||
finishPhotoRestorePlateDescription: 'L\'imprimante abaisse le plateau d\'environ 100 mm à la fin d\'une impression, plaçant l\'objet terminé sous le cadrage de la caméra. Bambuddy le remonte juste au-dessus de la dernière couche imprimée, prend la photo, puis le rabaisse. Ignoré si la hauteur d\'impression est inconnue ou si un autre travail est en file d\'attente.',
|
||||
ffmpegNotInstalled: 'ffmpeg non installé',
|
||||
ffmpegRequired: 'La capture caméra nécessite ffmpeg. Installez-le via <brew>brew install ffmpeg</brew> (macOS) ou <apt>apt install ffmpeg</apt> (Linux).',
|
||||
camera: 'Caméra',
|
||||
|
||||
@@ -2399,6 +2399,8 @@ export default {
|
||||
autoArchiveDescription: 'Salva automaticamente i file 3MF al completamento delle stampe',
|
||||
saveThumbnailsDescription: 'Estrai e salva le immagini di anteprima dai file 3MF',
|
||||
captureFinishPhotoDescription: 'Scatta una foto dalla fotocamera della stampante al completamento della stampa. Bambuddy registra un breve timelapse durante la stampa in modo che la foto possa essere ricavata dal momento precedente all\'abbassamento del piatto; il file del timelapse viene mantenuto se hai abilitato il timelapse per questa stampa, altrimenti viene eliminato automaticamente dopo l\'acquisizione della foto.',
|
||||
finishPhotoRestorePlate: 'Solleva il piatto per la foto finale',
|
||||
finishPhotoRestorePlateDescription: 'La stampante abbassa il piatto di circa 100 mm al termine di una stampa, lasciando l\'oggetto finito sotto l\'inquadratura della fotocamera. Bambuddy lo risolleva fino a poco sopra l\'ultimo strato stampato, scatta la foto e poi lo riabbassa. Ignorato se l\'altezza di stampa è sconosciuta o se un altro lavoro è in coda.',
|
||||
ffmpegNotInstalled: 'ffmpeg non installato',
|
||||
ffmpegRequired: 'L\'acquisizione dalla fotocamera richiede ffmpeg. Installalo tramite <brew>brew install ffmpeg</brew> (macOS) o <apt>apt install ffmpeg</apt> (Linux).',
|
||||
camera: 'Fotocamera',
|
||||
|
||||
@@ -2445,6 +2445,8 @@ export default {
|
||||
autoArchiveDescription: '印刷完了時に3MFファイルを自動保存',
|
||||
saveThumbnailsDescription: '3MFファイルからプレビュー画像を抽出して保存',
|
||||
captureFinishPhotoDescription: '印刷完了時にプリンターカメラから写真を撮影します。Bambuddy は印刷中に短いタイムラプスを記録し、ベッドが下がる前の瞬間から写真を取得できるようにします。この印刷でタイムラプスを有効にしていた場合はタイムラプスファイルが保存され、それ以外の場合は写真の取得後に自動的に削除されます。',
|
||||
finishPhotoRestorePlate: '完了写真のためにプレートを戻す',
|
||||
finishPhotoRestorePlateDescription: 'プリンターは印刷終了時にビルドプレートを約 100 mm 下降させるため、完成した造形物がカメラの画角より下に来ます。Bambuddy はプレートを最終印刷レイヤーのすぐ上まで戻して写真を撮影し、その後再び下降させます。造形高さが不明な場合や次のジョブがキューにある場合はスキップされます。',
|
||||
ffmpegNotInstalled: 'ffmpegがインストールされていません',
|
||||
ffmpegRequired: 'カメラ撮影にはffmpegが必要です。<brew>brew install ffmpeg</brew>(macOS)または<apt>apt install ffmpeg</apt>(Linux)でインストールしてください。',
|
||||
// Camera
|
||||
|
||||
@@ -2315,6 +2315,8 @@ export default {
|
||||
autoArchiveDescription: '인쇄 완료 시 3MF 파일 자동 저장',
|
||||
saveThumbnailsDescription: '3MF 파일에서 미리보기 이미지 추출 및 저장',
|
||||
captureFinishPhotoDescription: '인쇄 완료 시 프린터 카메라로 사진 촬영. Bambuddy는 인쇄 중 짧은 타임랩스를 기록하여 베드가 내려가기 전 순간에서 사진을 가져올 수 있도록 합니다. 이 인쇄에 대해 타임랩스를 활성화한 경우 타임랩스 파일이 보관되며, 그렇지 않으면 사진 촬영 후 자동으로 삭제됩니다.',
|
||||
finishPhotoRestorePlate: '완료 사진을 위해 베드 올리기',
|
||||
finishPhotoRestorePlateDescription: '프린터는 인쇄가 끝나면 베드를 약 100 mm 내리므로 완성된 출력물이 카메라 화각 아래에 놓입니다. Bambuddy는 베드를 마지막 인쇄 레이어 바로 위까지 다시 올려 사진을 찍은 뒤 다시 내립니다. 출력 높이를 알 수 없거나 다른 작업이 대기 중이면 건너뜁니다.',
|
||||
ffmpegNotInstalled: 'ffmpeg 미설치',
|
||||
ffmpegRequired: '카메라 캡처에 ffmpeg가 필요합니다. macOS에서는 <brew>brew install ffmpeg</brew>, Linux에서는 <apt>apt install ffmpeg</apt>로 설치하세요.',
|
||||
camera: '카메라',
|
||||
|
||||
@@ -2399,6 +2399,8 @@ export default {
|
||||
autoArchiveDescription: 'Salvar automaticamente arquivos 3MF quando impressões forem concluídas',
|
||||
saveThumbnailsDescription: 'Extrair e salvar imagens de pré-visualização dos arquivos 3MF',
|
||||
captureFinishPhotoDescription: 'Tirar foto da câmera da impressora quando a impressão for concluída. Bambuddy grava um timelapse curto durante a impressão para que a foto possa ser obtida do momento antes da mesa descer; o arquivo do timelapse é mantido se você habilitou o timelapse para esta impressão, caso contrário ele é excluído automaticamente após a captura da foto.',
|
||||
finishPhotoRestorePlate: 'Elevar a mesa para a foto final',
|
||||
finishPhotoRestorePlateDescription: 'A impressora baixa a mesa cerca de 100 mm ao fim de uma impressão, deixando a peça pronta abaixo do enquadramento da câmera. O Bambuddy a eleva novamente até logo acima da última camada impressa, tira a foto e depois a baixa de novo. Ignorado quando a altura da impressão é desconhecida ou há outro trabalho na fila.',
|
||||
ffmpegNotInstalled: 'ffmpeg não instalado',
|
||||
ffmpegRequired: 'A captura de câmera requer ffmpeg. Instale via <brew>brew install ffmpeg</brew> (macOS) ou <apt>apt install ffmpeg</apt> (Linux).',
|
||||
camera: 'Câmera',
|
||||
|
||||
@@ -2316,6 +2316,8 @@ export default {
|
||||
autoArchiveDescription: "Автоматически сохранять 3MF после завершения печати",
|
||||
saveThumbnailsDescription: "Извлекать и сохранять изображения предпросмотра из 3MF",
|
||||
captureFinishPhotoDescription: "Сделать снимок камерой принтера после завершения печати. Во время печати Bambuddy записывает короткий таймлапс, чтобы получить кадр до опускания стола. Если таймлапс был включён для задания, файл сохранится; иначе после получения снимка он будет автоматически удалён.",
|
||||
finishPhotoRestorePlate: "Поднимать стол для финального снимка",
|
||||
finishPhotoRestorePlateDescription: "По окончании печати принтер опускает стол примерно на 100 мм, и готовая модель оказывается ниже кадра камеры. Bambuddy поднимает стол обратно чуть выше последнего напечатанного слоя, делает снимок и снова опускает его. Пропускается, если высота печати неизвестна или в очереди есть другое задание.",
|
||||
ffmpegNotInstalled: "ffmpeg не установлен",
|
||||
ffmpegRequired: "Для захвата изображения требуется ffmpeg. Установите его командой <brew>brew install ffmpeg</brew> в macOS или <apt>apt install ffmpeg</apt> в Linux.",
|
||||
camera: "Камера",
|
||||
|
||||
@@ -2450,6 +2450,8 @@ export default {
|
||||
autoArchiveDescription: 'Baskılar tamamlandığında 3MF dosyalarını otomatik olarak kaydet',
|
||||
saveThumbnailsDescription: '3MF dosyalarından önizleme görüntülerini çıkar ve kaydet',
|
||||
captureFinishPhotoDescription: 'Baskı tamamlandığında yazıcı kamerasından bir fotoğraf çek. Bambuddy, baskı sırasında kısa bir zaman atlamalı kayıt yapar, böylece fotoğraf tabla inmeden önceki andan alınabilir. Bu baskı için zaman atlamalı kaydı etkinleştirdiyseniz dosya saklanır, aksi takdirde fotoğraf çekildikten sonra otomatik olarak silinir.',
|
||||
finishPhotoRestorePlate: 'Bitiş fotoğrafı için tablayı yükselt',
|
||||
finishPhotoRestorePlateDescription: 'Yazıcı, baskı bittiğinde tablayı yaklaşık 100 mm aşağı indirir ve tamamlanmış baskı kameranın çerçevesinin altında kalır. Bambuddy tablayı son basılan katmanın hemen üzerine geri kaldırır, fotoğrafı çeker ve ardından tekrar indirir. Baskı yüksekliği bilinmiyorsa veya kuyrukta başka bir iş varsa atlanır.',
|
||||
ffmpegNotInstalled: 'ffmpeg yüklü değil',
|
||||
ffmpegRequired: 'Kamera yakalama ffmpeg gerektirir. <brew>brew install ffmpeg</brew> (macOS) veya <apt>apt install ffmpeg</apt> (Linux) ile yükleyin.',
|
||||
// Kamera
|
||||
|
||||
@@ -2465,6 +2465,8 @@ export default {
|
||||
autoArchiveDescription: "Автоматично зберігати файли 3MF після завершення друку",
|
||||
saveThumbnailsDescription: "Витягніть і збережіть зображення попереднього перегляду з файлів 3MF.",
|
||||
captureFinishPhotoDescription: "Зробіть фотографію з камери принтера після завершення друку. Bambuddy записує короткий проміжок часу під час друку, щоб фотографію можна було отримати з моменту, коли стіл опускається; файл уповільненої зйомки зберігається, якщо ви ввімкнули уповільнену зйомку для цього друку, інакше він автоматично видаляється після зйомки фотографії.",
|
||||
finishPhotoRestorePlate: "Піднімати стіл для фінального знімка",
|
||||
finishPhotoRestorePlateDescription: "Після завершення друку принтер опускає стіл приблизно на 100 мм, і готова модель опиняється нижче кадру камери. Bambuddy піднімає стіл назад трохи вище останнього надрукованого шару, робить знімок і знову опускає його. Пропускається, якщо висота друку невідома або в черзі є інше завдання.",
|
||||
ffmpegNotInstalled: "ffmpeg не встановлено",
|
||||
ffmpegRequired: "Для зйомки камерою потрібен ffmpeg. Встановіть його за допомогою <brew>brew install ffmpeg</brew> (macOS) або <apt>apt install ffmpeg</apt> (Linux).",
|
||||
// Camera
|
||||
|
||||
@@ -2444,6 +2444,8 @@ export default {
|
||||
autoArchiveDescription: '打印完成时自动保存3MF文件',
|
||||
saveThumbnailsDescription: '从3MF文件中提取并保存预览图像',
|
||||
captureFinishPhotoDescription: '打印完成时从打印机摄像头拍照。Bambuddy 会在打印期间录制一段短延时摄影,以便从热床下降前的瞬间获取照片;如果您为本次打印启用了延时摄影,文件将保留,否则会在拍照完成后自动删除。',
|
||||
finishPhotoRestorePlate: '为完成照片抬升热床',
|
||||
finishPhotoRestorePlateDescription: '打印结束时打印机会将热床下降约 100 mm,使完成的模型落在相机取景范围之下。Bambuddy 会将热床抬回到最后一层打印高度略上方,拍摄照片后再次下降。若打印高度未知或队列中还有其他任务,则跳过此步骤。',
|
||||
ffmpegNotInstalled: '未安装ffmpeg',
|
||||
ffmpegRequired: '摄像头捕获需要ffmpeg。通过 <brew>brew install ffmpeg</brew>(macOS)或 <apt>apt install ffmpeg</apt>(Linux)安装。',
|
||||
camera: '摄像头',
|
||||
|
||||
@@ -2444,6 +2444,8 @@ export default {
|
||||
autoArchiveDescription: '列印完成時自動儲存3MF檔案',
|
||||
saveThumbnailsDescription: '從3MF檔案中提取並儲存預覽影像',
|
||||
captureFinishPhotoDescription: '列印完成時從印表機攝影機拍照。Bambuddy 會在列印期間錄製一段短縮時攝影,以便從熱床下降前的瞬間取得照片;如果您為本次列印啟用了縮時攝影,檔案將保留,否則會在拍照完成後自動刪除。',
|
||||
finishPhotoRestorePlate: '為完成照片抬升熱床',
|
||||
finishPhotoRestorePlateDescription: '列印結束時印表機會將熱床下降約 100 mm,使完成的模型落在相機取景範圍之下。Bambuddy 會將熱床抬回到最後一層列印高度略上方,拍攝照片後再次下降。若列印高度未知或佇列中還有其他任務,則跳過此步驟。',
|
||||
ffmpegNotInstalled: '未安裝ffmpeg',
|
||||
ffmpegRequired: '攝影機捕獲需要ffmpeg。透過 <brew>brew install ffmpeg</brew>(macOS)或 <apt>apt install ffmpeg</apt>(Linux)安裝。',
|
||||
camera: '攝影機',
|
||||
|
||||
@@ -959,6 +959,7 @@ export function SettingsPage() {
|
||||
settings.auto_archive !== localSettings.auto_archive ||
|
||||
settings.save_thumbnails !== localSettings.save_thumbnails ||
|
||||
settings.capture_finish_photo !== localSettings.capture_finish_photo ||
|
||||
(settings.finish_photo_restore_plate ?? true) !== (localSettings.finish_photo_restore_plate ?? true) ||
|
||||
settings.default_filament_cost !== localSettings.default_filament_cost ||
|
||||
settings.currency !== localSettings.currency ||
|
||||
settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
|
||||
@@ -1058,6 +1059,10 @@ export function SettingsPage() {
|
||||
auto_archive: localSettings.auto_archive,
|
||||
save_thumbnails: localSettings.save_thumbnails,
|
||||
capture_finish_photo: localSettings.capture_finish_photo,
|
||||
// #2547: `?? true` mirrors the toggle's own default, so an install
|
||||
// whose settings payload predates this field saves what the user is
|
||||
// actually looking at rather than `undefined`.
|
||||
finish_photo_restore_plate: localSettings.finish_photo_restore_plate ?? true,
|
||||
default_filament_cost: localSettings.default_filament_cost,
|
||||
currency: localSettings.currency,
|
||||
energy_cost_per_kwh: localSettings.energy_cost_per_kwh,
|
||||
@@ -1897,6 +1902,28 @@ export function SettingsPage() {
|
||||
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
|
||||
</label>
|
||||
</div>
|
||||
{/* #2547: only meaningful while finish photos are being taken at
|
||||
all, so it hangs off the toggle above rather than standing
|
||||
alone in the list. */}
|
||||
{localSettings.capture_finish_photo && (
|
||||
<div className="flex items-center justify-between pl-4 border-l-2 border-bambu-dark-tertiary">
|
||||
<div>
|
||||
<p className="text-white">{t('settings.finishPhotoRestorePlate')}</p>
|
||||
<p className="text-sm text-bambu-gray">
|
||||
{t('settings.finishPhotoRestorePlateDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={localSettings.finish_photo_restore_plate ?? true}
|
||||
onChange={(e) => updateSetting('finish_photo_restore_plate', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{localSettings.capture_finish_photo && ffmpegStatus && !ffmpegStatus.installed && (
|
||||
<div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
|
||||
Reference in New Issue
Block a user