|
|
@@ -42,17 +42,49 @@ export function resolveDesktopSlicer(
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * File types a slicer can be handed — both by the desktop URI handler and by
|
|
|
- * the in-app sidecar. Source geometry only: a sliced file is an output, and
|
|
|
- * neither slicer has anything to do with one.
|
|
|
+ * What each desktop slicer's protocol handler will actually load.
|
|
|
*
|
|
|
- * Lives here rather than beside either caller because both the File Manager
|
|
|
- * (which has a filename) and the 3D preview (which has a `LibraryFile.file_type`)
|
|
|
+ * These are not the formats the two applications can open — both take an STL
|
|
|
+ * from File > Import perfectly well. They are what survives the *URL handoff*,
|
|
|
+ * which is a narrower thing, and the two slicers disagree about it (#3029).
|
|
|
+ *
|
|
|
+ * Bambu Studio routes every `bambustudio://open?file=` and `bambustudioopen://`
|
|
|
+ * URL into `Plater::import_model_id`, which refuses any filename that is not
|
|
|
+ * `.3mf` before it makes the HTTP request at all — "Download failed, unknown
|
|
|
+ * file format." Handing it an STL could only ever fail, and the message names
|
|
|
+ * the format rather than the handoff, so the failure reads as a broken file.
|
|
|
+ *
|
|
|
+ * OrcaSlicer sends only MakerWorld links and `bambustudioopen://` down that
|
|
|
+ * same 3MF-only path (`Downloader::start_download`). Everything else — which is
|
|
|
+ * what we emit for it, `orcaslicer://open?file=` against our own host — goes to
|
|
|
+ * its generic downloader, which has no extension check at all. So STL and STEP
|
|
|
+ * work there.
|
|
|
+ *
|
|
|
+ * Source geometry only either way: a sliced file is an output, and neither
|
|
|
+ * slicer has anything to do with one.
|
|
|
+ *
|
|
|
+ * This lives here rather than beside any caller because the File Manager (which
|
|
|
+ * has a filename) and the 3D preview (which has a `LibraryFile.file_type`)
|
|
|
* decide the same thing about the same file. They used to hold separate lists,
|
|
|
* and the two disagreed — a card menu offered a desktop handoff for an STL
|
|
|
* whose own 3D preview showed "Open in Slicer" greyed out.
|
|
|
*/
|
|
|
-export const SLICEABLE_FILE_TYPES = ['3mf', 'stl', 'step', 'stp'] as const;
|
|
|
+export const DESKTOP_SLICEABLE_FILE_TYPES: Record<SlicerType, readonly string[]> = {
|
|
|
+ bambu_studio: ['3mf'],
|
|
|
+ orcaslicer: ['3mf', 'stl', 'step', 'stp'],
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * The formats `slicer` accepts over the handoff.
|
|
|
+ *
|
|
|
+ * Unknown values fall back to Bambu Studio's list rather than throwing, because
|
|
|
+ * that is what the handoff itself does: `openInSlicer` treats anything that is
|
|
|
+ * not exactly `orcaslicer` as Bambu Studio. `settings.open_in_slicer` comes off
|
|
|
+ * the API unvalidated, so the two have to agree on that.
|
|
|
+ */
|
|
|
+function desktopFormats(slicer: SlicerType): readonly string[] {
|
|
|
+ return DESKTOP_SLICEABLE_FILE_TYPES[slicer] ?? DESKTOP_SLICEABLE_FILE_TYPES.bambu_studio;
|
|
|
+}
|
|
|
|
|
|
/**
|
|
|
* The subset the *sidecar* can slice.
|
|
|
@@ -66,35 +98,40 @@ export const SLICEABLE_FILE_TYPES = ['3mf', 'stl', 'step', 'stp'] as const;
|
|
|
export const API_SLICEABLE_FILE_TYPES = ['3mf', 'stl'] as const;
|
|
|
|
|
|
/**
|
|
|
- * Does a `LibraryFile.file_type` name a sliceable source file?
|
|
|
+ * Can `slicer` be handed this `LibraryFile.file_type` over the protocol handler?
|
|
|
*
|
|
|
* The backend stores compound extensions whole — a sliced 3MF classifies as
|
|
|
* `gcode.3mf`, not `3mf` (`classify_file_type` in `api/routes/library.py`) — so
|
|
|
* membership alone is enough to exclude sliced output here.
|
|
|
+ *
|
|
|
+ * The slicer is a required argument on purpose: the answer genuinely differs
|
|
|
+ * between the two, and a default would quietly reintroduce the STL handoff that
|
|
|
+ * Bambu Studio cannot honour.
|
|
|
*/
|
|
|
-export function isSliceableFileType(fileType?: string | null): boolean {
|
|
|
+export function isSliceableFileType(fileType: string | null | undefined, slicer: SlicerType): boolean {
|
|
|
const normalized = (fileType || '').toLowerCase();
|
|
|
- return (SLICEABLE_FILE_TYPES as readonly string[]).includes(normalized);
|
|
|
+ return desktopFormats(slicer).includes(normalized);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Does a filename name a sliceable source file?
|
|
|
+ * Can `slicer` be handed this filename over the protocol handler?
|
|
|
*
|
|
|
* Checked against the name rather than a stored type, so the compound
|
|
|
* extensions have to be ruled out explicitly: `.gcode.3mf` ends with `.3mf`.
|
|
|
*/
|
|
|
-export function isSliceableFilename(filename: string): boolean {
|
|
|
+export function isSliceableFilename(filename: string, slicer: SlicerType): boolean {
|
|
|
const lower = filename.toLowerCase();
|
|
|
if (lower.endsWith('.gcode') || lower.endsWith('.gcode.3mf')) return false;
|
|
|
- return SLICEABLE_FILE_TYPES.some((ext) => lower.endsWith(`.${ext}`));
|
|
|
+ return desktopFormats(slicer).some((ext) => lower.endsWith(`.${ext}`));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Does a filename name something the slicer *sidecar* can slice?
|
|
|
*
|
|
|
- * Narrower than `isSliceableFilename` by exactly STEP — see
|
|
|
+ * Narrower than OrcaSlicer's handoff list by exactly STEP — see
|
|
|
* `API_SLICEABLE_FILE_TYPES`. Use this wherever the action posts to
|
|
|
- * `/library/files/{id}/slice`; use the wider one for the desktop handoff.
|
|
|
+ * `/library/files/{id}/slice`; use `isSliceableFilename` for the desktop
|
|
|
+ * handoff, which needs to know which slicer it is handing to.
|
|
|
*/
|
|
|
export function isApiSliceableFilename(filename: string): boolean {
|
|
|
const lower = filename.toLowerCase();
|