● feat(slicer): server-side slicing via OrcaSlicer / Bambu Studio sidecar

Adds an optional slicer-api/ Compose stack and wires Bambuddy's File
  Manager, Archives, and MakerWorld pages to a new server-side Slice flow.
  Slicing runs as an in-memory background job (POST returns 202 + job_id,
  polled via GET /api/v1/slice-jobs/{id}) so a multi-minute slice no
  longer pins the modal; result lands as a new .gcode.3mf in the same
  folder (or new archive for archive sources) with the embedded
  thumbnail extracted.

  Backend
  - New services: slice_dispatch (in-memory dispatcher, 30min retention
    sweep) and slicer_api (HTTP bridge with 4xx/5xx/connection error
    split that drives the 3MF embedded-settings fallback retry path).
  - New schemas: SliceRequest, SliceResponse, SliceArchiveResponse,
    SliceJobEnqueueResponse.
  - New routes: POST /library/files/{id}/slice,
    POST /archives/{id}/slice, GET /api/v1/slice-jobs/{id} (gated on
    LIBRARY_READ since job IDs are sequential and the body leaks source
    filenames and result IDs).
  - AppSettings + env defaults: use_slicer_api, orcaslicer_api_url,
    bambu_studio_api_url. DB-stored values override env defaults.

  Frontend
  - New SliceModal handles preset gating; enqueues then closes
    immediately.
  - New SliceJobTrackerProvider polls active jobs at app level, surfaces
    a single toast per job (queued -> running -> completed / failed)
    and invalidates library/archives queries on terminal status.
  - Settings -> Workflow -> Slicer card: preferred slicer dropdown,
    Use Slicer API toggle, contextual sidecar URL field.
  - File Manager / Archives / MakerWorld get a Slice button gated on
    the Use Slicer API setting.
  - gcode-viewer adapter learns ?library_file=<id> so sliced library
    files preview inline.

  i18n
  - New slice.* and settings.{useSlicerApi,slicerCard,orcaslicerApiUrl,
    bambuStudioApiUrl,slicerApiUrlDescription,useSlicerApiDescription}
    + fileManager.noPermissionSlice keys across all 8 locales (en, de,
    fr, it, ja, pt-BR, zh-CN, zh-TW). English fully translated, German
    fully translated, the other six seeded with English fallbacks
    pending native translation.

  Tests
  - 10 backend integration tests in test_library_slice_api.py covering
    validation (404/400), happy-path enqueue, sidecar-down, 3MF
    embedded-settings fallback, STL no-fallback, and preset-error ->
    failed job paths.
  - New unit tests in test_slicer_api.py for the HTTP bridge.
  - 5 new SliceModal frontend tests covering preset gating, library +
    archive enqueue paths, error surface, and preset-load failure.
  - Existing SettingsPage tests adjusted: slicer dropdown asserts now
    switch to the Workflow tab first; added a beforeEach URL reset so
    one test's tab click doesn't bleed into sibling tests.

  Sidecar
  - New slicer-api/ folder is self-contained and optional. Two services
    (orca-slicer-api on 3003, bambu-studio-api on 3001 behind --profile
    bambu) build via Docker git-build-context from
    maziggy/orca-slicer-api@bambuddy/profile-resolver. The fork patches
    the OrcaSlicer CLI's profile compatibility quirks (inherits-chain
    resolver, from:User -> system rewrite, '# ' clone-prefix strip,
    sentinel-value strip) empirically required to slice real GUI
    exports without segfaulting the CLI.

  Docs
  - CHANGELOG entry under [0.2.4b1] - Unreleased Added.
  - README File Manager bullet for the new server-side Slice button.
  - bambuddy-website features.html: new card under "Configurable Slicer".
  - bambuddy-wiki: new page features/slicer-api.md + nav entry +
    features index card.

  Notes
  - Opt-in: with Use Slicer API off, the existing "open in desktop
    slicer via URI" flow is the default and unchanged.
  - 3MF inputs that segfault the CLI on --load-settings transparently
    retry with embedded settings; the resulting job carries
    used_embedded_settings: true.
  - Sliced files always export as .gcode.3mf so File Manager picks up
    the embedded thumbnail; file_type is set to "gcode" (blue badge).
This commit is contained in:
maziggy
2026-04-27 15:28:37 +02:00
parent d92f48e3c6
commit 6deaa513af
40 changed files with 3556 additions and 539 deletions
+71
View File
@@ -926,6 +926,11 @@ export interface AppSettings {
camera_view_mode: 'window' | 'embedded';
// Preferred slicer
preferred_slicer: 'bambu_studio' | 'orcaslicer';
// Use the slicer-API sidecar for slicing (in-app modal) vs desktop URI scheme
use_slicer_api: boolean;
// Per-install sidecar URLs. Empty string falls back to the env defaults.
orcaslicer_api_url: string;
bambu_studio_api_url: string;
// Prometheus metrics
prometheus_enabled: boolean;
prometheus_token: string;
@@ -1105,6 +1110,57 @@ export interface BuiltinFilament {
name: string;
}
// Slice request/response — POST /library/files/{id}/slice and /archives/{id}/slice
export interface SliceRequest {
printer_preset_id: number;
process_preset_id: number;
filament_preset_id: number;
plate?: number;
export_3mf?: boolean;
}
export interface SliceResponse {
library_file_id: number;
name: string;
print_time_seconds: number;
filament_used_g: number;
filament_used_mm: number;
used_embedded_settings: boolean;
}
export interface SliceArchiveResponse {
archive_id: number;
name: string;
print_time_seconds: number;
filament_used_g: number;
filament_used_mm: number;
used_embedded_settings: boolean;
}
// Background slice-job lifecycle. POST /slice returns 202 + this shape;
// the frontend polls /slice-jobs/{id} until status is terminal.
export type SliceJobStatus = 'pending' | 'running' | 'completed' | 'failed';
export interface SliceJobEnqueueResponse {
job_id: number;
status: SliceJobStatus;
status_url: string;
}
export interface SliceJobState {
job_id: number;
status: SliceJobStatus;
kind: 'library_file' | 'archive';
source_id: number;
source_name: string;
created_at: string;
started_at: string | null;
completed_at: string | null;
result?: SliceResponse | SliceArchiveResponse;
error_status?: number;
error_detail?: string;
}
// Local preset types (OrcaSlicer imports)
export interface LocalPreset {
id: number;
@@ -4905,6 +4961,21 @@ export const api = {
body: JSON.stringify({ url }),
}),
// Slicer API — slice in the background. Both endpoints return 202 + a
// job_id; poll /slice-jobs/{id} until status is `completed` or `failed`.
sliceLibraryFile: (fileId: number, body: SliceRequest) =>
request<SliceJobEnqueueResponse>(`/library/files/${fileId}/slice`, {
method: 'POST',
body: JSON.stringify(body),
}),
sliceArchive: (archiveId: number, body: SliceRequest) =>
request<SliceJobEnqueueResponse>(`/archives/${archiveId}/slice`, {
method: 'POST',
body: JSON.stringify(body),
}),
getSliceJob: (jobId: number) =>
request<SliceJobState>(`/slice-jobs/${jobId}`),
// Local Presets (OrcaSlicer imports)
getLocalPresets: () =>
request<LocalPresetsResponse>('/local-presets/'),