mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
● 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:
@@ -180,6 +180,13 @@
|
||||
/^\/?downloads\/files\/local\/__bambuddy_archive_(\d+)$/,
|
||||
API_BASE + '/archives/$1/gcode'
|
||||
);
|
||||
// OctoPrint file download → Bambuddy library file gcode
|
||||
// (sliced LibraryFile — extracts embedded gcode from .gcode.3mf
|
||||
// or returns plain .gcode). Plate is ignored upstream for now.
|
||||
newPath = newPath.replace(
|
||||
/^\/?downloads\/files\/local\/__bambuddy_libgcode_(\d+)(?:_plate\d+)?$/,
|
||||
API_BASE + '/library/files/$1/gcode'
|
||||
);
|
||||
// OctoPrint plugin static assets → gcode-viewer static files
|
||||
newPath = newPath.replace(
|
||||
/^\/?plugin\/prettygcode\/static\//,
|
||||
@@ -411,6 +418,57 @@
|
||||
.catch(function () { /* best-effort — default bed stays */ });
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 10b. Library file loader — invoked via /gcode-viewer/?library_file=<id>
|
||||
// -------------------------------------------------------------------------
|
||||
function loadLibraryFileById(fileId, plate) {
|
||||
// Mirror loadArchiveById, but use the library gcode endpoint. The
|
||||
// /library/files/<id>/gcode endpoint extracts the embedded gcode
|
||||
// from a .gcode.3mf or returns a plain .gcode body. It does not
|
||||
// accept a plate selector today — multi-plate library files would
|
||||
// need an upstream change. The plate suffix here is preserved in
|
||||
// currentFilename for display only.
|
||||
var plateSuffix = (typeof plate === 'number' && plate >= 1) ? ('_plate' + plate) : '';
|
||||
currentFileId = 'libfile_' + fileId + plateSuffix;
|
||||
currentFilename = 'Library file #' + fileId + (plateSuffix ? (' (plate ' + plate + ')') : '');
|
||||
currentFileDate = Date.now();
|
||||
gcodeLayerMap = null;
|
||||
lastFedLayer = -1;
|
||||
stopPlayback(true);
|
||||
updateFilenameDisplay(currentFilename);
|
||||
var playBtn = document.getElementById('bb-play-btn');
|
||||
if (playBtn) playBtn.disabled = false;
|
||||
if (viewModel && viewModel.fromCurrentData) {
|
||||
viewModel.fromCurrentData({
|
||||
job: {
|
||||
file: {
|
||||
path: '__bambuddy_libgcode_' + fileId + plateSuffix,
|
||||
date: currentFileDate,
|
||||
},
|
||||
estimatedPrintTime: null,
|
||||
},
|
||||
state: { text: 'Operational', flags: { printing: false } },
|
||||
progress: { filepos: null, completion: 0 },
|
||||
currentZ: null,
|
||||
logs: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch metadata for the filename display. There is no
|
||||
// /library/files/<id>/capabilities endpoint, so the bed stays at
|
||||
// whatever the default fakePrinterProfile is set to.
|
||||
apiFetch('/library/files/' + fileId, {})
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (meta) {
|
||||
if (meta && (meta.print_name || meta.filename)) {
|
||||
currentFilename = (meta.print_name || meta.filename) +
|
||||
(plateSuffix ? (' (plate ' + plate + ')') : '');
|
||||
updateFilenameDisplay(currentFilename);
|
||||
}
|
||||
})
|
||||
.catch(function () { /* best-effort — filename stays "Library file #N" */ });
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 11. Initialise after DOM + scripts are ready
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -548,19 +606,23 @@
|
||||
function onDomReady() {
|
||||
setTimeout(function () {
|
||||
init();
|
||||
// If the viewer was opened with ?archive=<id>[&plate=<N>], load that
|
||||
// archive's gcode for the requested plate once the viewmodel is ready.
|
||||
// If the viewer was opened with ?archive=<id>[&plate=<N>] or
|
||||
// ?library_file=<id>[&plate=<N>], load that source's gcode once
|
||||
// the viewmodel is ready.
|
||||
try {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var archiveParam = params.get('archive');
|
||||
var libParam = params.get('library_file');
|
||||
var plateParam = params.get('plate');
|
||||
var plateId = (plateParam && /^[1-9][0-9]*$/.test(plateParam))
|
||||
? parseInt(plateParam, 10)
|
||||
: undefined;
|
||||
if (archiveParam && /^[1-9][0-9]*$/.test(archiveParam)) {
|
||||
var archiveId = parseInt(archiveParam, 10);
|
||||
var plateId = (plateParam && /^[1-9][0-9]*$/.test(plateParam))
|
||||
? parseInt(plateParam, 10)
|
||||
: undefined;
|
||||
// Allow a tick for init() to finish wiring viewModel.fromCurrentData
|
||||
setTimeout(function () { loadArchiveById(archiveId, plateId); }, 50);
|
||||
} else if (libParam && /^[1-9][0-9]*$/.test(libParam)) {
|
||||
var libId = parseInt(libParam, 10);
|
||||
setTimeout(function () { loadLibraryFileById(libId, plateId); }, 50);
|
||||
}
|
||||
} catch (e) { /* URLSearchParams unsupported — skip */ }
|
||||
}, 200);
|
||||
@@ -577,6 +639,7 @@
|
||||
// -------------------------------------------------------------------------
|
||||
window.BambuddyPrettyGCode = {
|
||||
loadArchive: loadArchiveById,
|
||||
loadLibraryFile: loadLibraryFileById,
|
||||
getViewModel: function () { return viewModel; },
|
||||
play: startPlayback,
|
||||
stop: stopPlayback,
|
||||
|
||||
Reference in New Issue
Block a user