feat(slice): cross-printer re-slicing across nozzle classes + multi-plate slice-all

Re-slicing a 3MF authored for a single-nozzle printer (X1C, P1S, A1, P2S)
  onto a dual-nozzle printer (H2D / H2D Pro) — or vice versa — previously
  failed with "G-code in unprintable area of multi-extruder printers" (the
  source's bed-coordinate layout lands in the H2D's per-nozzle dead zone)
  or, on multi-color projects, a hard SIGSEGV inside the slicer's ZFiller
  polygon-clipping. Earlier shipped a fail-fast 400 guard; this drop lifts
  it and actually does the conversion by forwarding the sidecar's existing
  --arrange flag when the source and target nozzle classes differ. BS
  itself reconciles the embedded project_settings.config against the new
  printer that way, the same way the GUI's "Switch Printer" operation
  does. The guard becomes a kept-for-compat no-op.

  Slice-all-plates added to the SliceModal: a checkbox for multi-plate
  sources sends plate=0 to the backend, which forwards --slice 0 to the
  BS CLI. Same-class slice-all produces one multi-plate output 3MF in a
  single sidecar call. Cross-class slice-all loops per plate (BS's
  --arrange is project-wide and would otherwise consolidate every plate's
  objects onto one bed) and merges the per-plate outputs into one
  multi-plate 3MF locally via the new merge_plate_3mfs helper. The toast
  shows "Plate 2 of 5 — Generating G-code (47%)" through the loop.

  Three side fixes surfaced during testing:
  - substitute_unused_plate_filaments overwrites unused-slot filaments
    with the slot-1 selection before slicing so BS's loaded-filament
    temperature validator doesn't reject a PLA print whose unused slot 2
    defaulted to ABS in the dropdown
  - re-sliced archive thumbnail now prefers the source's per-plate
    render (Metadata/plate_N.png) over the project-wide MakerWorld cover
    art, because BS CLI with --arrange skips writing a fresh per-plate
    preview
  - re-sliced archive bed_type now lifts from the sliced output's
    curr_bed_type onto the PrintArchive column the card actually reads

  Schema: SliceRequest.plate range relaxed from ge=1 to ge=0 to admit
  the "all plates" sentinel; SlicerApiService.slice_with_profiles /
  slice_with_bundle take an `arrange` parameter.

  Tests: 26 in test_slicer_3mf_convert (count / merge / substitute /
  extract), 3 in test_slicer_api (arrange wire format), 9 in
  test_library_slice_api (guard no-op, bed_type lift, thumbnail
  fallback, new cross-class slice-all loop integration test), 2 in
  test_archive_service (Auxiliaries fallback), 4 in SliceModal.test
  (plate=0 toggle), 2 in SliceJobTrackerContext.test (multi-plate toast
  prefix). 659 backend + 42 frontend green; backend ruff clean,
  frontend build clean, i18n parity green at 4984 keys × 9 locales.
This commit is contained in:
maziggy
2026-05-23 12:16:34 +02:00
parent ebba1385d3
commit 4686d108ef
27 changed files with 2024 additions and 307 deletions
@@ -543,6 +543,68 @@ describe('SliceModal', () => {
});
});
it('"Slice all plates" toggle sends plate=0 sentinel to the backend (#1493)', async () => {
mockApi.getLibraryFilePlates.mockResolvedValue(makeMultiPlateLibraryResponse());
mockApi.sliceLibraryFile.mockResolvedValue({
job_id: 42,
status: 'pending',
status_url: '/api/v1/slice-jobs/42',
});
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Multi.3mf' },
onClose: vi.fn(),
});
const user = userEvent.setup();
const plate1Button = await screen.findByRole('button', { name: /Plate 1.*Cube/ });
await user.click(plate1Button);
await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
// The "Slice all plates" checkbox only appears for multi-plate sources.
const toggle = await screen.findByRole('checkbox', { name: /Slice all 2 plates/i });
await user.click(toggle);
// The action button's label flips to the "Slice all" form. Click it.
await user.click(screen.getByRole('button', { name: /Slice all 2 plates/i }));
await waitFor(() => {
expect(mockApi.sliceLibraryFile).toHaveBeenCalledTimes(1);
});
const [, body] = mockApi.sliceLibraryFile.mock.calls[0];
// ``plate=0`` is the BS CLI's all-plates sentinel — one slice call,
// one output 3MF with every plate's gcode inside, one archive.
expect((body as { plate?: number }).plate).toBe(0);
});
it('"Slice all plates" toggle is hidden for single-plate sources', async () => {
mockApi.getLibraryFilePlates.mockResolvedValue({
file_id: 100,
filename: 'Single.3mf',
is_multi_plate: false,
plates: [
{
index: 1,
name: 'Plate 1',
objects: [],
has_thumbnail: false,
thumbnail_url: null,
print_time_seconds: null,
filament_used_grams: null,
filaments: [],
},
],
});
renderWithTracker({
source: { kind: 'libraryFile', id: 100, filename: 'Single.3mf' },
onClose: vi.fn(),
});
await waitFor(() => expect(screen.getByText('My Custom X1C')).toBeDefined());
expect(screen.queryByRole('checkbox', { name: /Slice all/i })).toBeNull();
});
it('routes the plate fetch through getArchivePlates for archive sources', async () => {
mockApi.getArchivePlates.mockResolvedValue({
...makeMultiPlateLibraryResponse(),
@@ -341,6 +341,105 @@ describe('SliceJobTrackerProvider — persistent progress toast', () => {
expect(text.textContent).toMatch(/75%/);
});
it('shows "Plate X of Y" prefix when the snapshot carries multi-plate loop fields (#1493)', async () => {
// Cross-class slice-all wraps each per-plate sub-slice's progress
// snapshot with multi_plate_index / multi_plate_count so the toast
// surfaces loop position alongside the slicer's stage message.
mockApi.getSliceJob.mockResolvedValue({
job_id: 9,
status: 'running',
kind: 'library_file',
source_id: 300,
source_name: 'Mewtwo.3mf',
created_at: new Date().toISOString(),
started_at: new Date().toISOString(),
completed_at: null,
progress: {
stage: 'Generating G-code',
total_percent: 47,
plate_percent: 47,
plate_index: 2,
plate_count: 5,
updated_at: Date.now(),
multi_plate_index: 2,
multi_plate_count: 5,
},
});
render(
<Wrapper>
<TrackTrigger id={9} name="Mewtwo.3mf" />
</Wrapper>,
);
act(() => {
screen.getByText('track-9').click();
});
await act(async () => {
vi.advanceTimersByTime(1500);
});
await act(async () => {
await Promise.resolve();
});
act(() => {
vi.advanceTimersByTime(1000);
});
// The augmented toast carries the "Plate 2 of 5" prefix.
const text = screen.getByText(/Plate 2 of 5/);
expect(text.textContent).toMatch(/Mewtwo\.3mf/);
expect(text.textContent).toMatch(/Generating G-code/);
expect(text.textContent).toMatch(/47%/);
});
it('does NOT add the "Plate X of Y" prefix when the loop fields are absent', async () => {
// Sanity: a normal single-plate slice (no slice-all loop) still
// renders the existing progress message, no "Plate" prefix.
mockApi.getSliceJob.mockResolvedValue({
job_id: 10,
status: 'running',
kind: 'library_file',
source_id: 301,
source_name: 'Single.3mf',
created_at: new Date().toISOString(),
started_at: new Date().toISOString(),
completed_at: null,
progress: {
stage: 'Generating G-code',
total_percent: 50,
plate_percent: 50,
plate_index: 1,
plate_count: 1,
updated_at: Date.now(),
},
});
render(
<Wrapper>
<TrackTrigger id={10} name="Single.3mf" />
</Wrapper>,
);
act(() => {
screen.getByText('track-10').click();
});
await act(async () => {
vi.advanceTimersByTime(1500);
});
await act(async () => {
await Promise.resolve();
});
act(() => {
vi.advanceTimersByTime(1000);
});
expect(screen.queryByText(/Plate \d+ of \d+/)).toBeNull();
const text = screen.getByText(/Generating G-code/);
expect(text.textContent).toMatch(/Single\.3mf/);
});
it('falls back to elapsed-time message when progress is null', async () => {
// Sidecar without --pipe support / pre-progress feature: state.progress
// stays null and the toast shows the existing "Slicing X — 47s" text.
+6
View File
@@ -1420,6 +1420,12 @@ export interface SliceJobProgress {
plate_index: number;
plate_count: number;
updated_at: number;
/** When the backend is in the cross-class slice-all loop (#1493), each
* per-plate sub-slice's progress is augmented with the loop position
* so the toast can show "Plate 2 of 5 — Generating G-code 47%". The
* fields are absent on a single-plate slice. */
multi_plate_index?: number;
multi_plate_count?: number;
}
export interface SliceJobState {
+98 -58
View File
@@ -333,6 +333,15 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
// and we'll backfill 1 at submit time). Set to a 1-indexed plate number once
// the user picks one (or implicitly for single-plate sources).
const [selectedPlate, setSelectedPlate] = useState<number | null>(null);
// "Slice all plates" mode: sends ``plate=0`` to the backend which forwards
// ``--slice 0`` to the BS CLI, producing a single output 3MF whose
// ``Metadata/plate_N.gcode`` entries are *all* plates sliced together —
// one archive, one file, all plates. Distinct from the per-plate
// ``selectedPlate`` mode (which slices just that one plate). Filament
// selection in this mode covers every slot the project defines, not
// just the slots the currently-visible plate happens to use — see
// ``allProjectFilamentSlots`` below.
const [sliceAllPlates, setSliceAllPlates] = useState(false);
// Build-plate override (#1337). null = inherit from the process preset
// (the default). Set to a canonical slicer enum value to patch
// curr_bed_type into the resolved process JSON before slicing — needed
@@ -393,14 +402,25 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
// Filament slot list for the active plate. Falls back to one synthetic slot
// for STL/STEP and any "no metadata available" case so the modal still
// works (single dropdown, mono-color slice).
// works (single dropdown, mono-color slice). In ``sliceAllPlates`` mode
// we keep the same slot list (the backend already returns every project
// slot via ``extract_project_filaments_from_3mf``'s fallback path when
// slice_info doesn't carry per-plate filaments) but override every
// slot's ``used_in_plate`` flag to ``true`` so the dropdown labels
// drop the "— not used by this plate" suffix and the dropdowns become
// selectable. Across the whole project, every defined slot IS used by
// at least one plate, so this is correct in slice-all mode.
const filamentSlots = useMemo<PlateFilament[]>(() => {
const reqs = filamentReqsQuery.data?.filaments ?? [];
if (reqs.length > 0) return reqs as PlateFilament[];
return [
{ slot_id: 1, type: '', color: '', used_grams: 0, used_meters: 0 },
];
}, [filamentReqsQuery.data]);
const base: PlateFilament[] =
reqs.length > 0
? (reqs as PlateFilament[])
: [{ slot_id: 1, type: '', color: '', used_grams: 0, used_meters: 0 }];
if (sliceAllPlates) {
return base.map((slot) => ({ ...slot, used_in_plate: true }));
}
return base;
}, [sliceAllPlates, filamentReqsQuery.data]);
const presetsQuery = useQuery({
queryKey: ['slicerPresets'],
@@ -537,57 +557,8 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
}, [selectedBundle, filamentSlots]);
const enqueueMutation = useMutation({
mutationFn: async () => {
let body: SliceRequest;
if (isBundleMode) {
// Bundle dispatch path. The selected bundle's first printer is the
// implicit printer choice (every .bbscfg carries exactly one).
if (
!selectedBundle ||
!bundleProcessName ||
bundleFilamentNames.length === 0 ||
bundleFilamentNames.some((n) => n == null)
) {
throw new Error(t('slice.bundleAllRequired'));
}
const bundleSpec: SliceBundleSpec = {
bundle_id: selectedBundle.id,
printer_name: selectedBundle.printer[0] ?? selectedBundle.printer_preset_name,
process_name: bundleProcessName,
filament_names: bundleFilamentNames as string[],
};
body = {
bundle: bundleSpec,
...(selectedPlate != null ? { plate: selectedPlate } : {}),
// Bed-type override (#1337) also flows through the bundle path —
// the sidecar forwards `bedType` as --curr_bed_type to the CLI.
...(bedType != null ? { bed_type: bedType } : {}),
};
} else {
if (
!printerPreset ||
!processPreset ||
filamentPresets.length === 0 ||
filamentPresets.some((r) => r == null)
) {
throw new Error(t('slice.allPresetsRequired'));
}
body = {
printer_preset: printerPreset,
process_preset: processPreset,
// The first slot also goes into the legacy singular field so the
// backend's older callers / clients keep behaving the same — the
// backend validator prefers `filament_presets` when both are set.
filament_preset: filamentPresets[0] as PresetRef,
filament_presets: filamentPresets as PresetRef[],
// Always send a concrete plate number when the source is multi-plate;
// omit otherwise so the backend default applies for STL / single-plate
// 3MF sources where the concept doesn't apply.
...(selectedPlate != null ? { plate: selectedPlate } : {}),
// Bed-type override (#1337).
...(bedType != null ? { bed_type: bedType } : {}),
};
}
mutationFn: async (plate: number | null) => {
const body = buildSliceBody(plate);
if (source.kind === 'libraryFile') {
return api.sliceLibraryFile(source.id, body);
}
@@ -603,6 +574,52 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
},
});
// Body builder shared by the single-plate and slice-all paths. ``plate``
// is the 1-indexed plate number to slice, or ``null`` for STL / single-
// plate 3MF sources where the field is omitted entirely.
function buildSliceBody(plate: number | null): SliceRequest {
if (isBundleMode) {
if (
!selectedBundle ||
!bundleProcessName ||
bundleFilamentNames.length === 0 ||
bundleFilamentNames.some((n) => n == null)
) {
throw new Error(t('slice.bundleAllRequired'));
}
const bundleSpec: SliceBundleSpec = {
bundle_id: selectedBundle.id,
printer_name: selectedBundle.printer[0] ?? selectedBundle.printer_preset_name,
process_name: bundleProcessName,
filament_names: bundleFilamentNames as string[],
};
return {
bundle: bundleSpec,
...(plate != null ? { plate } : {}),
// Bed-type override (#1337) also flows through the bundle path —
// the sidecar forwards `bedType` as --curr_bed_type to the CLI.
...(bedType != null ? { bed_type: bedType } : {}),
};
}
if (
!printerPreset ||
!processPreset ||
filamentPresets.length === 0 ||
filamentPresets.some((r) => r == null)
) {
throw new Error(t('slice.allPresetsRequired'));
}
return {
printer_preset: printerPreset,
process_preset: processPreset,
filament_preset: filamentPresets[0] as PresetRef,
filament_presets: filamentPresets as PresetRef[],
...(plate != null ? { plate } : {}),
...(bedType != null ? { bed_type: bedType } : {}),
};
}
// Slice button stays disabled until the preview slice / embedded-metadata
// read has succeeded (filamentReqsQuery.isSuccess) and every filament slot
// has a picked profile.
@@ -618,6 +635,8 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
filamentPresets.length > 0 &&
filamentPresets.every((r) => r != null);
const isEnqueuing = enqueueMutation.isPending;
const totalPlateCount = platesQuery.data?.plates?.length ?? 0;
const canSliceAll = isMultiPlate && totalPlateCount > 1 && !needsPlatePicker;
// Step 1: plate picker for multi-plate 3MF sources. Cancelling closes the
// entire flow (matches the existing PlatePickerModal contract used by the
@@ -870,11 +889,30 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
>
{t('common.cancel')}
</button>
{canSliceAll && (
<label
className="flex items-center gap-2 mr-auto text-sm text-bambu-gray cursor-pointer select-none"
title={t('slice.actionAllTitle', { count: totalPlateCount })}
>
<input
type="checkbox"
checked={sliceAllPlates}
onChange={(e) => setSliceAllPlates(e.target.checked)}
disabled={isEnqueuing}
className="cursor-pointer"
/>
{t('slice.allPlatesToggle', { count: totalPlateCount })}
</label>
)}
<button
type="button"
onClick={() => {
setErrorMessage(null);
enqueueMutation.mutate();
// ``plate=0`` is the sidecar's "all plates" sentinel — passes
// ``--slice 0`` to the BS CLI which produces a single 3MF
// with one ``Metadata/plate_N.gcode`` entry per plate.
const platePayload = sliceAllPlates ? 0 : selectedPlate;
enqueueMutation.mutate(platePayload);
}}
disabled={!isReady || isEnqueuing}
className="px-3 py-1.5 text-sm rounded-md bg-bambu-green hover:bg-bambu-green/90 text-bambu-dark font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
@@ -884,6 +922,8 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
<Loader2 className="w-4 h-4 animate-spin" />
{t('slice.enqueuing')}
</>
) : sliceAllPlates ? (
t('slice.actionAll', { count: totalPlateCount })
) : (
t('slice.action')
)}
@@ -102,20 +102,36 @@ export function SliceJobTrackerProvider({ children }: { children: ReactNode }) {
// --pipe support.
const hasUseful = progress && progress.stage && progress.total_percent > 0;
if (phase === 'running' && hasUseful) {
showPersistentToast(
toastIdFor(job.id),
t(
'slice.runningWithProgress',
'{{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
{
name: prettifyFilename(job.sourceName),
stage: progress.stage,
percent: Math.min(100, Math.max(0, Math.round(progress.total_percent))),
elapsed: elapsedStr,
},
),
'loading',
);
const name = prettifyFilename(job.sourceName);
const stage = progress.stage;
const percent = Math.min(100, Math.max(0, Math.round(progress.total_percent)));
// Cross-class slice-all (#1493) feeds the same toast through N
// sequential per-plate slices; the augmented snapshot tells us
// which plate is currently running so the user sees the loop
// progress, not just a single repeating bar.
const isMultiPlateLoop =
typeof progress.multi_plate_index === 'number' &&
typeof progress.multi_plate_count === 'number' &&
progress.multi_plate_count > 1;
const message = isMultiPlateLoop
? t(
'slice.runningWithProgressMultiPlate',
'Plate {{plateIndex}} of {{plateCount}} • {{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
{
plateIndex: progress.multi_plate_index,
plateCount: progress.multi_plate_count,
name,
stage,
percent,
elapsed: elapsedStr,
},
)
: t(
'slice.runningWithProgress',
'{{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
{ name, stage, percent, elapsed: elapsedStr },
);
showPersistentToast(toastIdFor(job.id), message, 'loading');
return;
}
const messageKey = phase === 'pending' ? 'slice.queuedToast' : 'slice.runningToast';
+4
View File
@@ -3449,6 +3449,9 @@ export default {
slice: {
title: 'Modell slicen',
action: 'Slicen',
actionAll: 'Alle {{count}} Plates slicen',
actionAllTitle: 'Alle Plates in eine einzelne Multi-Plate-Ausgabe slicen (ein Archiv). Die Filamentauswahl gilt für jeden Slot des Projekts.',
allPlatesToggle: 'Alle {{count}} Plates slicen',
slicing: 'Slicen…',
printer: 'Drucker-Profil',
process: 'Prozess-Profil',
@@ -3475,6 +3478,7 @@ export default {
queuedToast: 'Warteschlange: {{name}} — {{elapsed}}',
runningToast: '{{name}} wird gesliced — {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}} %) – {{elapsed}}',
runningWithProgressMultiPlate: 'Plate {{plateIndex}} von {{plateCount}} • {{name}} – {{stage}} ({{percent}} %) – {{elapsed}}',
completedToast: '{{name}} wurde gesliced',
failedTitle: 'Slicen fehlgeschlagen',
failedToast: 'Slicen von {{name}} fehlgeschlagen: {{detail}}',
+4
View File
@@ -3452,6 +3452,9 @@ export default {
slice: {
title: 'Slice model',
action: 'Slice',
actionAll: 'Slice all {{count}} plates',
actionAllTitle: 'Slice every plate into one multi-plate output (single archive). Filament selection covers every slot the project defines.',
allPlatesToggle: 'Slice all {{count}} plates',
slicing: 'Slicing…',
printer: 'Printer profile',
process: 'Process profile',
@@ -3478,6 +3481,7 @@ export default {
queuedToast: 'Queued: {{name}} — {{elapsed}}',
runningToast: 'Slicing {{name}} — {{elapsed}}',
runningWithProgress: '{{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
runningWithProgressMultiPlate: 'Plate {{plateIndex}} of {{plateCount}} • {{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
completedToast: 'Sliced {{name}}',
failedTitle: 'Slicing failed',
failedToast: 'Slicing {{name}} failed: {{detail}}',
+4
View File
@@ -3452,6 +3452,9 @@ export default {
slice: {
title: 'Laminar modelo',
action: 'Laminar',
actionAll: 'Laminar las {{count}} bandejas',
actionAllTitle: 'Laminar todas las bandejas en una salida única de varias bandejas (un solo archivo). La selección de filamento cubre cada hueco que defina el proyecto.',
allPlatesToggle: 'Laminar las {{count}} bandejas',
slicing: 'Laminando…',
printer: 'Perfil de impresora',
process: 'Perfil de proceso',
@@ -3478,6 +3481,7 @@ export default {
queuedToast: 'En cola: {{name}} — {{elapsed}}',
runningToast: 'Laminando {{name}} — {{elapsed}}',
runningWithProgress: '{{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
runningWithProgressMultiPlate: 'Bandeja {{plateIndex}} de {{plateCount}} • {{name}} — {{stage}} ({{percent}}%) — {{elapsed}}',
completedToast: '{{name}} laminado',
failedTitle: 'Error al laminar',
failedToast: 'Error al laminar {{name}}: {{detail}}',
+4
View File
@@ -3438,6 +3438,9 @@ export default {
slice: {
title: 'Slicer le modèle',
action: 'Slicer',
actionAll: 'Slicer les {{count}} plateaux',
actionAllTitle: "Slicer tous les plateaux dans une sortie multi-plateaux (une seule archive). La sélection de filament couvre chaque emplacement défini par le projet.",
allPlatesToggle: 'Slicer les {{count}} plateaux',
slicing: 'Découpage…',
printer: 'Profil d\'imprimante',
process: 'Profil de processus',
@@ -3464,6 +3467,7 @@ export default {
queuedToast: 'En attente : {{name}} – {{elapsed}}',
runningToast: 'Découpage {{name}} – {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}} %) – {{elapsed}}',
runningWithProgressMultiPlate: 'Plateau {{plateIndex}} sur {{plateCount}} • {{name}} – {{stage}} ({{percent}} %) – {{elapsed}}',
completedToast: '{{name}} découpé',
failedTitle: 'Échec du découpage',
failedToast: 'Échec du découpage de {{name}} : {{detail}}',
+4
View File
@@ -3437,6 +3437,9 @@ export default {
slice: {
title: 'Slicing modello',
action: 'Slice',
actionAll: 'Slicia tutti i {{count}} piatti',
actionAllTitle: 'Slicia tutti i piatti in un unico output multi-piatto (un solo archivio). La selezione del filamento copre ogni slot definito dal progetto.',
allPlatesToggle: 'Slicia tutti i {{count}} piatti',
slicing: 'Slicing…',
printer: 'Profilo stampante',
process: 'Profilo processo',
@@ -3463,6 +3466,7 @@ export default {
queuedToast: 'In coda: {{name}} – {{elapsed}}',
runningToast: 'Slicing {{name}} – {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
runningWithProgressMultiPlate: 'Piatto {{plateIndex}} di {{plateCount}} • {{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
completedToast: '{{name}} sezionato',
failedTitle: 'Slicing fallito',
failedToast: 'Slicing di {{name}} fallito: {{detail}}',
+4
View File
@@ -3449,6 +3449,9 @@ export default {
slice: {
title: 'モデルをスライス',
action: 'スライス',
actionAll: '{{count}} プレートすべてをスライス',
actionAllTitle: 'すべてのプレートを 1 つのマルチプレート出力としてスライスします(アーカイブは 1 件)。フィラメントの選択はプロジェクトで定義されたすべてのスロットを対象とします。',
allPlatesToggle: '{{count}} プレートすべてをスライス',
slicing: 'スライス中…',
printer: 'プリンタープロファイル',
process: 'プロセスプロファイル',
@@ -3475,6 +3478,7 @@ export default {
queuedToast: '待機中: {{name}} – {{elapsed}}',
runningToast: '{{name}}をスライス中 – {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
runningWithProgressMultiPlate: 'プレート {{plateIndex}} / {{plateCount}} • {{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
completedToast: '{{name}}をスライス済み',
failedTitle: 'スライスに失敗しました',
failedToast: '{{name}}のスライスに失敗: {{detail}}',
+4
View File
@@ -3437,6 +3437,9 @@ export default {
slice: {
title: 'Fatiar modelo',
action: 'Fatiar',
actionAll: 'Fatiar todas as {{count}} bandejas',
actionAllTitle: 'Fatiar todas as bandejas em uma saída multi-bandeja única (um único arquivo). A seleção de filamento cobre cada slot definido pelo projeto.',
allPlatesToggle: 'Fatiar todas as {{count}} bandejas',
slicing: 'Fatiando…',
printer: 'Perfil de impressora',
process: 'Perfil de processo',
@@ -3463,6 +3466,7 @@ export default {
queuedToast: 'Na fila: {{name}} – {{elapsed}}',
runningToast: 'Fatiando {{name}} – {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
runningWithProgressMultiPlate: 'Bandeja {{plateIndex}} de {{plateCount}} • {{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
completedToast: '{{name}} fatiado',
failedTitle: 'Falha ao fatiar',
failedToast: 'Falha ao fatiar {{name}}: {{detail}}',
+4
View File
@@ -3437,6 +3437,9 @@ export default {
slice: {
title: '切片模型',
action: '切片',
actionAll: '切片全部 {{count}} 个盘面',
actionAllTitle: '将所有盘面切片为单个多盘面输出(一个归档)。耗材选择覆盖项目定义的每个槽位。',
allPlatesToggle: '切片全部 {{count}} 个盘面',
slicing: '切片中…',
printer: '打印机配置',
process: '工艺配置',
@@ -3463,6 +3466,7 @@ export default {
queuedToast: '已排队:{{name}} — {{elapsed}}',
runningToast: '切片 {{name}} — {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
runningWithProgressMultiPlate: '盘面 {{plateIndex}} / {{plateCount}} • {{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
completedToast: '已切片 {{name}}',
failedTitle: '切片失败',
failedToast: '切片 {{name}} 失败:{{detail}}',
+4
View File
@@ -3437,6 +3437,9 @@ export default {
slice: {
title: '切片模型',
action: '切片',
actionAll: '切片全部 {{count}} 個盤面',
actionAllTitle: '將所有盤面切片為單一的多盤面輸出(單一封存)。線材選擇涵蓋專案定義的每個槽位。',
allPlatesToggle: '切片全部 {{count}} 個盤面',
slicing: '切片中…',
printer: '印表機設定檔',
process: '製程設定檔',
@@ -3463,6 +3466,7 @@ export default {
queuedToast: '已排隊:{{name}} — {{elapsed}}',
runningToast: '切片 {{name}} — {{elapsed}}',
runningWithProgress: '{{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
runningWithProgressMultiPlate: '盤面 {{plateIndex}} / {{plateCount}} • {{name}} – {{stage}} ({{percent}}%) – {{elapsed}}',
completedToast: '已切片 {{name}}',
failedTitle: '切片失敗',
failedToast: '切片 {{name}} 失敗:{{detail}}',