feat(print-options): add "Auto" state to bed levelling, flow & nozzle-offset calibration

Bed levelling, flow calibration, and nozzle-offset calibration were on/off
only, so the sole way to run bed levelling was to force a full level before
every print. Bambu Studio has always offered a third "Auto" state that lets
the printer skip the calibration when it was done recently -- the state most
users actually want. Make these three options tri-state (off/on/auto),
defaulting to auto, and leave vibration/layer-inspect/timelapse as on/off
(Bambu Studio exposes no auto for those).

Wire encoding follows Bambu Studio's source exactly: each option sends a JSON
bool (true only for "on") plus a companion int -- off=0, on=1, auto=2. The
bool fields stay booleans (the #1478 H2S regression); only the companion int
widened from {0,1} to {0,1,2}. #1721's observation that stage 8/39 stays
queued when sending 2 is the auto contract (queued, skipped at runtime if
recent), not a broken "off".

- schemas: TriState = Literal[off/on/auto] with a BeforeValidator coercing
  legacy bool / 0-1 / true-false so old clients and un-migrated rows validate
- model + migration: boolean columns -> String; SQLite via column affinity +
  data backfill, PostgreSQL via ALTER COLUMN TYPE guarded on information_schema
  (verified on both dialects); settings rows normalised true/false -> on/off
- MQTT: start_print takes the tri-state strings and emits the paired bool+int
- Virtual Printer: reconstructs the slicer's auto/on/off from the int companion
  (auto_bed_leveling / extrude_cali_flag) in both capture paths
- frontend: CalibrationMode type; off/auto/on segmented controls in the print
  dialog, queue bulk-edit, and Settings -> Workflow; calibrationMode_* strings
  in all 11 locales
This commit is contained in:
maziggy
2026-07-20 17:55:56 +02:00
parent 454ce3ceec
commit 2e45893dd5
41 changed files with 817 additions and 343 deletions
@@ -1,15 +1,30 @@
import { useState } from 'react';
import { Settings, ChevronDown, ChevronUp, Flame } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { PrintOptionsProps, PrintOptions as PrintOptionsType, PreheatOverride } from './types';
import type {
PrintOptionsProps,
PrintOptions as PrintOptionsType,
PreheatOverride,
CalibrationMode,
} from './types';
import {
CALIBRATION_MODES,
CALIBRATION_MODE_ACTIVE,
CALIBRATION_MODE_INACTIVE,
} from '../../utils/calibrationMode';
type OptionConfig = {
key: keyof PrintOptionsType;
label: string;
desc: string;
dualNozzleOnly?: boolean;
/** Tri-state (off/on/auto) rather than a plain on/off pair. */
tristate?: boolean;
};
// On/off options render as the same button pair, minus the "auto" choice.
const BOOLEAN_MODES = ['off', 'on'] as const;
/**
* Print options toggle panel with collapsible UI.
* Shows bed levelling, flow/vibration calibration, layer inspection, timelapse,
@@ -27,18 +42,22 @@ export function PrintOptionsPanel({
// Labels/descriptions reuse the settings.default* namespace — identical strings,
// already translated across all locales. Only nozzle_offset_cali is new (#1682).
const printOptionsConfig: OptionConfig[] = [
{ key: 'bed_levelling', label: t('settings.defaultBedLevelling'), desc: t('settings.defaultBedLevellingDesc') },
{ key: 'flow_cali', label: t('settings.defaultFlowCali'), desc: t('settings.defaultFlowCaliDesc') },
{ key: 'bed_levelling', label: t('settings.defaultBedLevelling'), desc: t('settings.defaultBedLevellingDesc'), tristate: true },
{ key: 'flow_cali', label: t('settings.defaultFlowCali'), desc: t('settings.defaultFlowCaliDesc'), tristate: true },
{ key: 'vibration_cali', label: t('settings.defaultVibrationCali'), desc: t('settings.defaultVibrationCaliDesc') },
{ key: 'layer_inspect', label: t('settings.defaultLayerInspect'), desc: t('settings.defaultLayerInspectDesc') },
{ key: 'timelapse', label: t('settings.defaultTimelapse'), desc: t('settings.defaultTimelapseDesc') },
{ key: 'nozzle_offset_cali', label: t('settings.defaultNozzleOffsetCali'), desc: t('settings.defaultNozzleOffsetCaliDesc'), dualNozzleOnly: true },
{ key: 'nozzle_offset_cali', label: t('settings.defaultNozzleOffsetCali'), desc: t('settings.defaultNozzleOffsetCaliDesc'), dualNozzleOnly: true, tristate: true },
];
const visibleOptions = printOptionsConfig.filter(o => !o.dualNozzleOnly || showDualNozzleOptions);
const handleToggle = (key: keyof PrintOptionsType) => {
onChange({ ...options, [key]: !options[key] });
const handleToggle = (key: keyof PrintOptionsType, value: boolean) => {
onChange({ ...options, [key]: value });
};
const handleCalibrationMode = (key: keyof PrintOptionsType, mode: CalibrationMode) => {
onChange({ ...options, [key]: mode });
};
const handlePreheatOverride = (next: PreheatOverride) => {
@@ -81,26 +100,56 @@ export function PrintOptionsPanel({
</button>
{isExpanded && (
<div className="mt-2 bg-bambu-dark rounded-lg p-3 space-y-2">
{visibleOptions.map(({ key, label, desc }) => (
<label key={key} className="flex items-center justify-between cursor-pointer group">
<div>
<span className="text-sm text-white">{label}</span>
<p className="text-xs text-bambu-gray">{desc}</p>
{visibleOptions.map(({ key, label, desc, tristate }) =>
tristate ? (
<div key={key} className="flex items-center justify-between gap-3">
<div>
<span className="text-sm text-white">{label}</span>
<p className="text-xs text-bambu-gray">{desc}</p>
</div>
<div className="flex gap-1 shrink-0">
{CALIBRATION_MODES.map((mode) => (
<button
key={mode}
type="button"
onClick={() => handleCalibrationMode(key, mode)}
className={`px-2.5 py-1 text-xs rounded transition-colors ${
options[key as 'bed_levelling'] === mode
? CALIBRATION_MODE_ACTIVE[mode]
: CALIBRATION_MODE_INACTIVE
}`}
>
{t(`settings.calibrationMode_${mode}`)}
</button>
))}
</div>
</div>
<div
className={`relative w-10 h-5 rounded-full transition-colors ${
options[key as 'bed_levelling'] ? 'bg-bambu-green' : 'bg-bambu-dark-tertiary'
}`}
onClick={() => handleToggle(key)}
>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
options[key as 'bed_levelling'] ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
) : (
<div key={key} className="flex items-center justify-between gap-3">
<div>
<span className="text-sm text-white">{label}</span>
<p className="text-xs text-bambu-gray">{desc}</p>
</div>
<div className="flex gap-1 shrink-0">
{BOOLEAN_MODES.map((mode) => {
const active = (options[key as 'vibration_cali'] ? 'on' : 'off') === mode;
return (
<button
key={mode}
type="button"
onClick={() => handleToggle(key, mode === 'on')}
className={`px-2.5 py-1 text-xs rounded transition-colors ${
active ? CALIBRATION_MODE_ACTIVE[mode] : CALIBRATION_MODE_INACTIVE
}`}
>
{t(`settings.calibrationMode_${mode}`)}
</button>
);
})}
</div>
</div>
</label>
))}
),
)}
{/* Preheat / heat-soak per-item override (#1468). Defaults to
'inherit' which means the global Settings → Workflow toggle
+9 -7
View File
@@ -1,4 +1,6 @@
import type { PrintQueueItem, Printer } from '../../api/client';
import type { PrintQueueItem, Printer, CalibrationMode } from '../../api/client';
export type { CalibrationMode };
/**
* Mode of operation for the PrintModal.
@@ -44,12 +46,12 @@ export interface PrintModalProps {
export type PreheatOverride = 'inherit' | 'on' | 'off';
export interface PrintOptions {
bed_levelling: boolean;
flow_cali: boolean;
bed_levelling: CalibrationMode;
flow_cali: CalibrationMode;
vibration_cali: boolean;
layer_inspect: boolean;
timelapse: boolean;
nozzle_offset_cali: boolean;
nozzle_offset_cali: CalibrationMode;
// Per-item preheat / heat-soak override (#1468). 'inherit' uses the global
// Settings → Workflow toggle; 'on' / 'off' force the per-print decision.
// chamber_target_override is non-null to bypass the per-filament-type
@@ -62,12 +64,12 @@ export interface PrintOptions {
* Default print options values.
*/
export const DEFAULT_PRINT_OPTIONS: PrintOptions = {
bed_levelling: true,
flow_cali: false,
bed_levelling: 'auto',
flow_cali: 'auto',
vibration_cali: true,
layer_inspect: false,
timelapse: false,
nozzle_offset_cali: true,
nozzle_offset_cali: 'auto',
preheat_override: 'inherit',
preheat_chamber_target_override: null,
};