Pool AMS backup spools in the print dialog's filament check

The dialog weighed each plate against the spool in the slot it mapped to
and knew nothing about AMS Filament Backup, so a two-plate job needing
1441 g of ABS was refused against a 1000 g spool while the identical full
spool in the next slot went uncounted. The dispatcher has pooled matching
spools since #1762 and would have run the print -- "Print anyway" was
always the right answer to this warning.

The rule for which spools back each other up now lives in one place:
build_slot_materials() in filament_deficit, which the dispatcher's pool
and the new slot_materials half of GET /printers/{id}/inventory-remain
both draw on. The dialog groups on the keys it is handed rather than
resolving spools a second time, which is what let the two answers drift
apart, and which also gives the check to Spoolman users -- it read the
internal inventory only, so in Spoolman mode it approved everything.

Where a pool really is short the warning quotes the pooled totals, since
the per-slot figure reads as a contradiction next to a full peer spool.
This commit is contained in:
maziggy
2026-08-11 11:32:17 +02:00
parent e93f1b43c3
commit df5aa04df1
24 changed files with 672 additions and 136 deletions
@@ -1811,14 +1811,13 @@ describe('PrintModal — per-plate filament mapping (#2551 follow-up)', () => {
if (plateId === '2') return HttpResponse.json({ filaments: [{ ...SLOT_2_RED, used_grams: 40 }] });
return HttpResponse.json({ filaments: [SLOT_1_RED, SLOT_2_RED] });
}),
http.get('/api/v1/inventory/assignments', () =>
HttpResponse.json([
{
id: 1, spool_id: 1, printer_id: 1, printer_name: 'X1 Carbon', ams_id: 0, tray_id: 0,
fingerprint_color: null, fingerprint_type: null, configured: true,
spool: { id: 1, label_weight: 1000, weight_used: 940 },
},
]),
http.get('/api/v1/printers/:id/inventory-remain', () =>
HttpResponse.json({
inventory_remain_g: { '0': 60 },
slot_materials: [
{ ams_id: 0, tray_id: 0, global_tray_id: 0, material_key: 'preset:GFA00|color:FF0000', remaining_g: 60, extruder: 0 },
],
}),
),
);
@@ -1836,6 +1835,103 @@ describe('PrintModal — per-plate filament mapping (#2551 follow-up)', () => {
expect(screen.getByText(/needs 80g, remaining 60g/i)).toBeInTheDocument();
});
// Two red trays holding the same material, so AMS Filament Backup can switch
// between them mid-print. Both tests below map both plates onto tray 0.
const twoRedTrays = (backupOn: boolean) =>
http.get('/api/v1/printers/:id/status', () =>
HttpResponse.json({
connected: true,
state: 'IDLE',
ams_filament_backup: backupOn,
ams: [{ id: 0, tray: [
{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' },
{ id: 1, tray_type: 'PLA', tray_color: 'FF0000' },
] }],
vt_tray: [],
}),
);
const redPool = (trayZeroGrams: number, trayOneGrams: number) =>
http.get('/api/v1/printers/:id/inventory-remain', () =>
HttpResponse.json({
inventory_remain_g: { '0': trayZeroGrams, '1': trayOneGrams },
slot_materials: [
{ ams_id: 0, tray_id: 0, global_tray_id: 0, material_key: 'preset:GFA00|color:FF0000', remaining_g: trayZeroGrams, extruder: 0 },
{ ams_id: 0, tray_id: 1, global_tray_id: 1, material_key: 'preset:GFA00|color:FF0000', remaining_g: trayOneGrams, extruder: 0 },
],
}),
);
const bothPlatesNeedForty = http.get('/api/v1/archives/:id/filament-requirements', ({ request }) => {
const plateId = new URL(request.url).searchParams.get('plate_id');
if (plateId === '1') return HttpResponse.json({ filaments: [{ ...SLOT_1_RED, used_grams: 40 }] });
if (plateId === '2') return HttpResponse.json({ filaments: [{ ...SLOT_2_RED, used_grams: 40 }] });
return HttpResponse.json({ filaments: [SLOT_1_RED, SLOT_2_RED] });
});
it('lets the print through when AMS Filament Backup covers it from a peer spool', async () => {
// The reported block: two plates draw 80 g from a tray holding 60 g, but the
// slot next to it holds the same material. The firmware switches between
// them, and the dispatcher has pooled them since #1762 — the modal used to
// weigh the mapped slot alone and refused a print the dispatcher would run.
const queued: unknown[] = [];
server.use(
bothPlatesNeedForty,
twoRedTrays(true),
redPool(60, 1000),
http.post('/api/v1/queue/', async ({ request }) => {
queued.push(await request.json());
return HttpResponse.json({ id: queued.length, status: 'pending' });
}),
);
const user = userEvent.setup();
render(<PrintModal mode="create" archiveId={1} archiveName="Two.gcode.3mf" onClose={mockOnClose} />);
await selectBothPlates(user);
await waitFor(() => expect(screen.getByText(/Filament Mapping — Plate 2/)).toBeInTheDocument());
await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
await waitFor(() => expect(queued.length).toBe(2));
expect(screen.queryByText(/Not enough filament/i)).not.toBeInTheDocument();
});
it('warns against the pooled total when AMS Filament Backup still cannot cover it', async () => {
// Backup is on but both spools together are short. The shortfall is real, so
// it still has to be raised — against the pool, not the one slot, or the
// numbers contradict the AMS the user is looking at.
server.use(bothPlatesNeedForty, twoRedTrays(true), redPool(60, 10));
const user = userEvent.setup();
render(<PrintModal mode="create" archiveId={1} archiveName="Two.gcode.3mf" onClose={mockOnClose} />);
await selectBothPlates(user);
await waitFor(() => expect(screen.getByText(/Filament Mapping — Plate 2/)).toBeInTheDocument());
await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
await waitFor(() => expect(screen.getByText(/Not enough filament/i)).toBeInTheDocument());
expect(screen.getByText(/needs 80g, 70g available across matching spools/i)).toBeInTheDocument();
});
it('keeps weighing the mapped slot alone when AMS Filament Backup is off', async () => {
// Same two spools, backup off: the firmware will not switch, so the peer's
// grams are not available to this print and the warning must still fire.
server.use(bothPlatesNeedForty, twoRedTrays(false), redPool(60, 1000));
const user = userEvent.setup();
render(<PrintModal mode="create" archiveId={1} archiveName="Two.gcode.3mf" onClose={mockOnClose} />);
await selectBothPlates(user);
await waitFor(() => expect(screen.getByText(/Filament Mapping — Plate 2/)).toBeInTheDocument());
await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
await waitFor(() => expect(screen.getByText(/Not enough filament/i)).toBeInTheDocument());
expect(screen.getByText(/needs 80g, remaining 60g/i)).toBeInTheDocument();
});
it('holds the Print button until every selected plate has answered', async () => {
// A plate whose filaments cannot be read has no mapping and no forced colours;
// queueing it anyway prints it in whatever happens to be loaded.
+3
View File
@@ -476,6 +476,9 @@ export const handlers = [
http.get('/api/v1/notification-templates', () => HttpResponse.json([])),
http.get('/api/v1/pending-uploads/', () => HttpResponse.json([])),
http.get('/api/v1/printers/:id/ams-labels', () => HttpResponse.json([])),
http.get('/api/v1/printers/:id/inventory-remain', () =>
HttpResponse.json({ inventory_remain_g: {}, slot_materials: [] }),
),
http.get('/api/v1/printers/:id/slot-presets', () => HttpResponse.json([])),
http.get('/api/v1/smart-plugs/by-printer/:id', () => HttpResponse.json([])),
http.get('/api/v1/smart-plugs/by-printer/:id/scripts', () => HttpResponse.json([])),
+22 -1
View File
@@ -3465,6 +3465,27 @@ export interface SpoolKProfileInput {
setting_id?: string | null;
}
/** One inventory-bound AMS slot, as returned by `/printers/{id}/inventory-remain`. */
export interface SlotMaterial {
ams_id: number;
tray_id: number;
global_tray_id: number;
/** Opaque grouping key from the backend. Two slots back each other up under
* AMS Filament Backup only when both `material_key` and `extruder` match.
* Never parse it — the format belongs to the backend's identity rule. */
material_key: string;
remaining_g: number;
/** 0 = right / single nozzle, 1 = left. */
extruder: number;
}
export interface InventoryRemainResponse {
/** Currently-loaded, inventory-bound slots only — drives the prefer-lowest sort. */
inventory_remain_g: Record<string, number>;
/** Every inventory binding on the printer, with identity + extruder side. */
slot_materials: SlotMaterial[];
}
export interface SpoolAssignment {
id: number;
spool_id: number;
@@ -4503,7 +4524,7 @@ export const api = {
// when computing the AMS mapping; mirrors backend `_build_inventory_remain_overrides`
// so internal and Spoolman modes both work uniformly.
getInventoryRemain: (printerId: number) =>
request<{ inventory_remain_g: Record<string, number> }>(
request<InventoryRemainResponse>(
`/printers/${printerId}/inventory-remain`,
),
+95 -48
View File
@@ -2,7 +2,7 @@ import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/rea
import { AlertCircle, AlertTriangle, Loader2, Pencil, Printer, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { CostCenterSummary, PrinterStatus, PrintQueueItemCreate, PrintQueueItemUpdate, SpoolAssignment } from '../../api/client';
import type { CostCenterSummary, PrinterStatus, PrintQueueItemCreate, PrintQueueItemUpdate, SlotMaterial } from '../../api/client';
import { api } from '../../api/client';
import { useAuth } from '../../contexts/AuthContext';
import { Card, CardContent } from '../Card';
@@ -21,7 +21,7 @@ import { isGcodeCompatible } from '../../utils/printer';
import { getCurrencySymbol } from '../../utils/currency';
import { getBedTypeInfo } from '../../utils/bedType';
import { toDateTimeLocalValue, parseUTCDate } from '../../utils/date';
import { getGlobalTrayId, isPlaceholderDate, effectivePreferLowest } from '../../utils/amsHelpers';
import { isPlaceholderDate, effectivePreferLowest } from '../../utils/amsHelpers';
import { resolveArchiveSlicerAmsMapping } from './archiveAmsMapping';
import { FilamentMapping } from './FilamentMapping';
import { FilamentOverride } from './FilamentOverride';
@@ -97,6 +97,9 @@ export function PrintModal({
slotLabel: string;
requiredGrams: number;
remainingGrams: number;
/** True when AMS Filament Backup pooled more than one spool for this slot;
* `requiredGrams` / `remainingGrams` are then the pooled totals. */
pooled?: boolean;
};
// Multiple printer selection (used for all modes now)
@@ -339,13 +342,6 @@ export function PrintModal({
setSelectedCostCenterId(preferredPrivate?.id ?? printableCostCenters[0].id);
}, [printableCostCenters, selectedCostCenterId]);
const { data: spoolAssignments } = useQuery({
queryKey: ['spool-assignments'],
queryFn: () => api.getAssignments(),
staleTime: 30 * 1000,
enabled: !isEditing && assignmentMode === 'printer',
});
// Fetch per-printer Map<globalTrayId, gramsRemaining> via the dedicated
// backend endpoint (#1766). Server-side mirrors `_build_inventory_remain_overrides`
// so internal and Spoolman modes both work uniformly, VT/external slots are
@@ -374,6 +370,23 @@ export function PrintModal({
return result;
}, [selectedPrinters, inventoryRemainQueries]);
// Same endpoint, the other half of its payload: every inventory-bound slot on
// the printer with the backend's material identity and extruder side. The
// pre-flight filament check groups on these instead of resolving spools
// itself, which is what makes it agree with the dispatcher and work in
// Spoolman mode (where the modal has no assignment rows of its own).
const slotMaterialsPerPrinter = useMemo(() => {
const result = new Map<number, Map<number, SlotMaterial>>();
selectedPrinters.forEach((printerId, idx) => {
const slots = inventoryRemainQueries[idx]?.data?.slot_materials;
if (!slots) return;
const printerMap = new Map<number, SlotMaterial>();
slots.forEach((slot) => printerMap.set(slot.global_tray_id, slot));
result.set(printerId, printerMap);
});
return result;
}, [selectedPrinters, inventoryRemainQueries]);
// Fetch archive details to get sliced_for_model
const { data: archiveDetails } = useQuery({
queryKey: ['archive', archiveId],
@@ -747,32 +760,26 @@ export function PrintModal({
const isMultiPlate = platesData?.is_multi_plate ?? false;
const plates = platesData?.plates ?? [];
const spoolAssignmentsByPrinter = useMemo(() => {
const map = new Map<number, Map<number, SpoolAssignment>>();
if (!spoolAssignments) return map;
spoolAssignments.forEach((assignment) => {
const isExternal = assignment.ams_id === 255;
const globalTrayId = getGlobalTrayId(
assignment.ams_id,
assignment.tray_id,
isExternal
);
const printerMap = map.get(assignment.printer_id) ?? new Map();
printerMap.set(globalTrayId, assignment);
map.set(assignment.printer_id, printerMap);
});
return map;
}, [spoolAssignments]);
const filamentWarningMessage = useMemo(() => {
if (!filamentWarningItems || filamentWarningItems.length === 0) return '';
const lines = filamentWarningItems.map((item) =>
t('printModal.insufficientFilamentLine', {
printer: item.printerName,
slot: item.slotLabel,
required: Math.round(item.requiredGrams),
remaining: Math.round(item.remainingGrams),
})
// Under AMS Filament Backup the shortfall is against the pooled spools,
// not the one slot — quoting that slot's remaining next to a pooled
// requirement reads as a contradiction ("needs 1441g, remaining 1000g"
// while a second full spool sits next to it).
item.pooled
? t('printModal.insufficientFilamentLinePooled', {
printer: item.printerName,
slot: item.slotLabel,
required: Math.round(item.requiredGrams),
remaining: Math.round(item.remainingGrams),
})
: t('printModal.insufficientFilamentLine', {
printer: item.printerName,
slot: item.slotLabel,
required: Math.round(item.requiredGrams),
remaining: Math.round(item.remainingGrams),
})
);
return [t('printModal.insufficientFilamentMessage'), ...lines].join('\n');
}, [filamentWarningItems, t]);
@@ -846,13 +853,7 @@ export function PrintModal({
? selectedPlateIds.map((plateId) => ({ plateId, reqs: perPlateReqs.get(plateId)?.filaments ?? [] }))
: [{ plateId: selectedPlate, reqs: effectiveFilamentReqs?.filaments ?? [] }];
if (plateJobs.some((job) => job.reqs.length > 0) && spoolAssignmentsByPrinter.size > 0) {
const getRemainingWeight = (labelWeight: number, weightUsed: number) => {
if (!Number.isFinite(labelWeight) || labelWeight <= 0) return null;
if (!Number.isFinite(weightUsed) || weightUsed < 0) return null;
return Math.max(0, labelWeight - weightUsed);
};
if (plateJobs.some((job) => job.reqs.length > 0) && slotMaterialsPerPrinter.size > 0) {
for (const printerId of selectedPrinters) {
const printerStatusForWarning = selectedPrinters.length > 1
? multiPrinterMapping.printerResults.find((result) => result.printerId === printerId)?.status
@@ -860,10 +861,13 @@ export function PrintModal({
const loadedFilaments = buildLoadedFilaments(printerStatusForWarning);
const slotLabelByTray = new Map(loadedFilaments.map((f) => [f.globalTrayId, f.label]));
const assignments = spoolAssignmentsByPrinter.get(printerId);
// Slots the backend could price. A slot missing here is one with no
// inventory binding, or one whose Spoolman spool it could not read —
// both mean "nothing to weigh", never "empty".
const slotMaterials = slotMaterialsPerPrinter.get(printerId);
const printerName = printers?.find((p) => p.id === printerId)?.name ?? `Printer ${printerId}`;
if (!assignments) continue;
if (!slotMaterials || slotMaterials.size === 0) continue;
const gramsByTray = new Map<number, number>();
for (const job of plateJobs) {
@@ -880,19 +884,62 @@ export function PrintModal({
});
}
// With AMS Filament Backup ON the firmware switches to any other slot
// holding the same material, so the print is only short when the whole
// pool is (#1762). The dispatcher has accounted for this since #1762 —
// this check did not, and blocked prints the dispatcher would have run.
// Dual-extruder printers pool per side: the firmware cannot cross
// nozzles even with the backup bit set, which is why `extruder` is part
// of the key the backend hands us.
const backupOn = printerStatusForWarning?.ams_filament_backup === true;
const poolKey = (slot: SlotMaterial) => `${slot.material_key}#${slot.extruder}`;
const pooledGrams = new Map<string, number>();
const pooledSlotCount = new Map<string, number>();
const pooledRequired = new Map<string, number>();
if (backupOn) {
slotMaterials.forEach((slot) => {
const key = poolKey(slot);
pooledGrams.set(key, (pooledGrams.get(key) ?? 0) + slot.remaining_g);
pooledSlotCount.set(key, (pooledSlotCount.get(key) ?? 0) + 1);
});
for (const [globalTrayId, requiredGrams] of gramsByTray) {
const slot = slotMaterials.get(globalTrayId);
if (!slot) continue;
const key = poolKey(slot);
pooledRequired.set(key, (pooledRequired.get(key) ?? 0) + requiredGrams);
}
}
for (const [globalTrayId, requiredGrams] of gramsByTray) {
const spool = assignments.get(globalTrayId)?.spool;
if (!spool) continue;
const slot = slotMaterials.get(globalTrayId);
if (!slot) continue;
const remainingGrams = getRemainingWeight(spool.label_weight, spool.weight_used);
if (remainingGrams === null) continue;
if (remainingGrams >= requiredGrams) continue;
const slotLabel = slotLabelByTray.get(globalTrayId) ?? `Tray ${globalTrayId}`;
if (backupOn) {
const key = poolKey(slot);
const available = pooledGrams.get(key) ?? 0;
const needed = pooledRequired.get(key) ?? 0;
if (available >= needed) continue;
warningItems.push({
printerName,
slotLabel,
requiredGrams: needed,
remainingGrams: available,
// A pool of one is just the slot itself — same numbers, so use
// the plain wording rather than talk about spools that aren't there.
pooled: (pooledSlotCount.get(key) ?? 1) > 1,
});
continue;
}
if (slot.remaining_g >= requiredGrams) continue;
warningItems.push({
printerName,
slotLabel: slotLabelByTray.get(globalTrayId) ?? `Tray ${globalTrayId}`,
slotLabel,
requiredGrams,
remainingGrams,
remainingGrams: slot.remaining_g,
});
}
}
+1
View File
@@ -4934,6 +4934,7 @@ export default {
waitingForAmsStatus: 'Warte auf AMS-Status von {{printer}}…',
insufficientFilamentMessage: 'Einige zugewiesene Spulen haben weniger Filament als dieser Druck benötigt:',
insufficientFilamentLine: '{{printer}} - {{slot}}: benötigt {{required}}g, verbleibend {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: benötigt {{required}}g, {{remaining}}g über passende Spulen verfügbar (AMS Filament Backup)',
printAnyway: 'Trotzdem drucken',
forceColorMatch: 'Farbe erzwingen',
staggerPrinterStarts: 'Druckerstarts staffeln',
+1
View File
@@ -4977,6 +4977,7 @@ export default {
waitingForAmsStatus: 'Waiting for AMS status from {{printer}}…',
insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:',
insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: needs {{required}}g, {{remaining}}g available across matching spools (AMS Filament Backup)',
printAnyway: 'Print anyway',
forceColorMatch: 'Force color match',
staggerPrinterStarts: 'Stagger printer starts',
+1
View File
@@ -4941,6 +4941,7 @@ export default {
waitingForAmsStatus: 'Esperando el estado del AMS de {{printer}}…',
insufficientFilamentMessage: 'Algunas bobinas asignadas tienen menos filamento restante del que necesita esta impresión:',
insufficientFilamentLine: '{{printer}} - {{slot}}: necesita {{required}} g, restante {{remaining}} g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: necesita {{required}} g, {{remaining}} g disponibles entre bobinas equivalentes (AMS Filament Backup)',
printAnyway: 'Imprimir de todos modos',
forceColorMatch: 'Forzar la coincidencia de color',
staggerPrinterStarts: 'Escalonar los inicios de las impresoras',
+1
View File
@@ -4923,6 +4923,7 @@ export default {
waitingForAmsStatus: 'En attente de l\'état de l\'AMS de {{printer}}…',
insufficientFilamentMessage: 'Certaines bobines assignées ont moins de filament restant que nécessaire pour cette impression :',
insufficientFilamentLine: '{{printer}} - {{slot}} : nécessite {{required}}g, restant {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}} : nécessite {{required}}g, {{remaining}}g disponibles sur les bobines correspondantes (AMS Filament Backup)',
printAnyway: 'Imprimer quand même',
forceColorMatch: 'Forcer correspondance des couleurs',
staggerPrinterStarts: 'Échelonner démarrages',
+1
View File
@@ -4922,6 +4922,7 @@ export default {
waitingForAmsStatus: 'In attesa dello stato AMS di {{printer}}…',
insufficientFilamentMessage: 'Alcune bobine assegnate hanno meno filamento rimanente di quanto necessario per questa stampa:',
insufficientFilamentLine: '{{printer}} - {{slot}}: necessita di {{required}}g, rimanenti {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: necessita di {{required}}g, {{remaining}}g disponibili tra le bobine corrispondenti (AMS Filament Backup)',
printAnyway: 'Stampa comunque',
forceColorMatch: 'Forza corrispondenza colore',
staggerPrinterStarts: 'Scaglionare avvii',
+1
View File
@@ -4934,6 +4934,7 @@ export default {
waitingForAmsStatus: '{{printer}} のAMSステータスを待機しています…',
insufficientFilamentMessage: '割り当てられたスプールの一部は、この印刷に必要な量より残量が少ないです:',
insufficientFilamentLine: '{{printer}} - {{slot}}: 必要 {{required}}g、残り {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: 必要 {{required}}g、一致するスプール合計で {{remaining}}g(AMSフィラメントバックアップ)',
printAnyway: 'それでも印刷',
forceColorMatch: 'カラーマッチを強制',
staggerPrinterStarts: 'プリンター開始を段階的に',
+1
View File
@@ -4704,6 +4704,7 @@ export default {
waitingForAmsStatus: '{{printer}}의 AMS 상태를 기다리는 중…',
insufficientFilamentMessage: '일부 할당된 스풀에 이 인쇄에 필요한 것보다 적은 필라멘트가 남아 있습니다:',
insufficientFilamentLine: '{{printer}} - {{slot}}: {{required}}g 필요, {{remaining}}g 남음',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: {{required}}g 필요, 일치하는 스풀 합계 {{remaining}}g (AMS 필라멘트 백업)',
printAnyway: '그래도 인쇄',
forceColorMatch: '색상 일치 강제',
staggerPrinterStarts: '프린터 시작 분산',
+1
View File
@@ -4922,6 +4922,7 @@ export default {
waitingForAmsStatus: 'Aguardando o status do AMS de {{printer}}…',
insufficientFilamentMessage: 'Alguns dos carretéis atribuídos têm menos filamento restante do que o necessário para esta impressão:',
insufficientFilamentLine: '{{printer}} - {{slot}}: necessário {{required}}g, restante {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: necessário {{required}}g, {{remaining}}g disponíveis entre bobinas correspondentes (AMS Filament Backup)',
printAnyway: 'Imprimir mesmo assim',
forceColorMatch: 'Forçar correspondência de cor',
staggerPrinterStarts: 'Escalonar inícios',
+1
View File
@@ -4693,6 +4693,7 @@ export default {
waitingForAmsStatus: "Ожидание состояния AMS от принтера {{printer}}…",
insufficientFilamentMessage: "На некоторых назначенных катушках осталось меньше филамента, чем требуется для этой печати:",
insufficientFilamentLine: "{{printer}} — {{slot}}: требуется {{required}} г, осталось {{remaining}} г",
insufficientFilamentLinePooled: "{{printer}} — {{slot}}: требуется {{required}} г, доступно {{remaining}} г на подходящих катушках (резервный филамент AMS)",
printAnyway: "Всё равно печатать",
forceColorMatch: "Обязательно учитывать цвет",
staggerPrinterStarts: "Запускать принтеры с задержкой",
+1
View File
@@ -4911,6 +4911,7 @@ export default {
waitingForAmsStatus: '{{printer}} için AMS durumu bekleniyor…',
insufficientFilamentMessage: 'Bazı atanmış makaralarda bu baskının ihtiyaç duyduğundan daha az filament kaldı:',
insufficientFilamentLine: '{{printer}} - {{slot}}: {{required}}g gerekli, {{remaining}}g kaldı',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}: {{required}}g gerekli, eşleşen makaralarda toplam {{remaining}}g (AMS Filament Backup)',
printAnyway: 'Yine de yazdır',
forceColorMatch: 'Renk eşleşmesini zorla',
staggerPrinterStarts: 'Yazıcı başlangıçlarını kademelendir',
+1
View File
@@ -4976,6 +4976,7 @@ export default {
waitingForAmsStatus: "Очікування статусу AMS від {{printer}}…",
insufficientFilamentMessage: "У деяких призначених котушках залишилося менше філаменту, ніж потрібно для друку:",
insufficientFilamentLine: "{{printer}} - {{slot}}: потрібно {{required}}g, залишилося {{remaining}}g",
insufficientFilamentLinePooled: "{{printer}} - {{slot}}: потрібно {{required}}g, доступно {{remaining}}g на відповідних котушках (резервне перемикання філаменту AMS)",
printAnyway: "Усе одно друкувати",
forceColorMatch: "Примусовий збіг кольорів",
staggerPrinterStarts: "Запускати принтери почергово",
+1
View File
@@ -4922,6 +4922,7 @@ export default {
waitingForAmsStatus: '正在等待 {{printer}} 的 AMS 状态…',
insufficientFilamentMessage: '部分已分配线轴的剩余耗材少于本次打印所需:',
insufficientFilamentLine: '{{printer}} - {{slot}}:需要 {{required}}g,剩余 {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}:需要 {{required}}g,匹配料卷共剩余 {{remaining}}g(AMS 备用料盘)',
printAnyway: '仍然打印',
forceColorMatch: '强制颜色匹配',
staggerPrinterStarts: '错峰启动打印机',
+1
View File
@@ -4922,6 +4922,7 @@ export default {
waitingForAmsStatus: '正在等待 {{printer}} 的 AMS 狀態…',
insufficientFilamentMessage: '部分已分配料盤的剩餘耗材少於本次列印所需:',
insufficientFilamentLine: '{{printer}} - {{slot}}:需要 {{required}}g,剩餘 {{remaining}}g',
insufficientFilamentLinePooled: '{{printer}} - {{slot}}:需要 {{required}}g,相符料卷共剩餘 {{remaining}}g(AMS 備用料盤)',
printAnyway: '仍然列印',
forceColorMatch: '強制顏色匹配',
staggerPrinterStarts: '錯開印表機啟動',