Просмотр исходного кода

fix(library): render 3D preview for sliced .gcode.3mf files (#1543)

  Reporter exported a multi-plate .gcode.3mf from Bambu Studio to the
  shared folder Bambuddy watches and the 3D preview tab came up empty;
  if he re-uploaded the same file via the file manager, the preview
  worked. Root cause: two paths classify file_type differently. The
  shared-folder scan at backend/app/api/routes/library.py:1343-1348
  does a compound-extension check and tags the file `gcode.3mf`; the
  upload path at the same file's :1588 does a single `ext[1:]` and tags
  it `3mf`. Then frontend/src/components/ModelViewerModal.tsx:71-73 had

    hasModel = normalizedType === '3mf' || 'stl'
    hasGcode = normalizedType === 'gcode' || '3mf'

  Neither matched `gcode.3mf`, so the capabilities object landed with
  both flags false and the modal rendered an empty bed.
  FileManagerPage.tsx:858 also gated the Preview-3D context action on
  `file_type === '3mf' || 'gcode' || 'stl'`, so for shared-folder files
  the menu entry didn't even appear, and the type pill at :765-770 had
  no colour case for `gcode.3mf` so it fell through to the generic
  gray.

  Fix (frontend-only, no backend churn):

  - ModelViewerModal.tsx introduces an
    `isThreeMfFamily = normalizedType === '3mf' || normalizedType === 'gcode.3mf'`
    predicate used in two branches — the capabilities check
    (`hasModel = isThreeMfFamily || 'stl'`, `hasGcode = isThreeMfFamily
    || 'gcode'`) and the plates-loading branch that previously hard-
    gated on `!== '3mf'` and would have returned setPlatesData(null)
    for the shared-folder file.

  - FileManagerPage.tsx adds `gcode.3mf` to the Preview-3D action gate
    and shares the gcode blue type-pill colour so sliced-output files
    are visually distinguishable from source 3MFs.

  The compound `gcode.3mf` classification on the backend is intentionally
  preserved — it carries useful "this is a sliced output" semantics that
  other UI surfaces could use later. The `canOpenInSlicer` and
  `sliceableType` checks at ModelViewerModal.tsx:269, 277-280 are
  deliberately left alone — a sliced output isn't openable in the slicer,
  and `sliceableType` already explicitly excludes `.gcode` and
  `.gcode.3mf` per the comment.

  Out of scope (separate Bambu-Studio format limitation, not a Bambuddy
  bug): Vlado's secondary observation that the upload-path 3D preview
  "shows only one plate" even though his project has 5 plates — Bambu
  Studio's .gcode.3mf export contains the model data and g-code for the
  active plate only, not the entire multi-plate project. The print
  picker enumerates plates via gcode_*.gcode entries inside the zip
  (a separate code path), which is why the user can still pick the
  plate at print time. The empty-bed fix is the data point that closes
  the user-visible bug.
maziggy 2 дней назад
Родитель
Сommit
40cd45e2d5

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 13 - 3
frontend/src/components/ModelViewerModal.tsx

@@ -69,8 +69,15 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
 
 
     if (isLibrary) {
     if (isLibrary) {
       const normalizedType = (fileType || '').toLowerCase();
       const normalizedType = (fileType || '').toLowerCase();
-      const hasModel = normalizedType === '3mf' || normalizedType === 'stl';
-      const hasGcode = normalizedType === 'gcode' || normalizedType === '3mf';
+      // A `.gcode.3mf` file is the slicer's sliced output — it carries
+      // both the per-plate model (in `3D/3dmodel.model`) and the g-code
+      // for the active plate (in `Metadata/plate_*.gcode`). The backend
+      // library scan path (library.py) tags it `gcode.3mf` while the
+      // upload path tags it `3mf`, so we accept both shapes here for
+      // the 3D-tab + g-code-tab gating (#1543).
+      const isThreeMfFamily = normalizedType === '3mf' || normalizedType === 'gcode.3mf';
+      const hasModel = isThreeMfFamily || normalizedType === 'stl';
+      const hasGcode = isThreeMfFamily || normalizedType === 'gcode';
       setCapabilities({
       setCapabilities({
         has_model: hasModel,
         has_model: hasModel,
         has_gcode: hasGcode,
         has_gcode: hasGcode,
@@ -116,7 +123,10 @@ export function ModelViewerModal({ archiveId, libraryFileId, title, fileType, on
 
 
     if (isLibrary) {
     if (isLibrary) {
       const normalizedType = (fileType || '').toLowerCase();
       const normalizedType = (fileType || '').toLowerCase();
-      if (!libraryFileId || normalizedType !== '3mf') {
+      // Same 3mf-family gate as the capabilities branch above — sliced
+      // `.gcode.3mf` files have plate metadata too (#1543).
+      const isThreeMfFamily = normalizedType === '3mf' || normalizedType === 'gcode.3mf';
+      if (!libraryFileId || !isThreeMfFamily) {
         setPlatesData(null);
         setPlatesData(null);
         setPlatesLoading(false);
         setPlatesLoading(false);
         return;
         return;

+ 4 - 2
frontend/src/pages/FileManagerPage.tsx

@@ -763,7 +763,9 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload,
         {/* File type badge */}
         {/* File type badge */}
         <div className={`absolute top-2 right-2 text-xs px-1.5 py-0.5 rounded font-medium ${
         <div className={`absolute top-2 right-2 text-xs px-1.5 py-0.5 rounded font-medium ${
           file.file_type === '3mf' ? 'bg-bambu-green/90 text-white'
           file.file_type === '3mf' ? 'bg-bambu-green/90 text-white'
-          : file.file_type === 'gcode' ? 'bg-blue-500/90 text-white'
+          // Sliced output — share the gcode blue so users see at a glance
+          // that the file is already sliced and ready to print (#1543).
+          : file.file_type === 'gcode' || file.file_type === 'gcode.3mf' ? 'bg-blue-500/90 text-white'
           : file.file_type === 'stl' ? 'bg-purple-500/90 text-white'
           : file.file_type === 'stl' ? 'bg-purple-500/90 text-white'
           : 'bg-bambu-gray/90 text-white'
           : 'bg-bambu-gray/90 text-white'
         }`}>
         }`}>
@@ -855,7 +857,7 @@ function FileCard({ file, isSelected, isMobile, onSelect, onDelete, onDownload,
                   {t('slice.action')}
                   {t('slice.action')}
                 </button>
                 </button>
               )}
               )}
-              {onPreview3d && (file.file_type === '3mf' || file.file_type === 'gcode' || file.file_type === 'stl') && (
+              {onPreview3d && (file.file_type === '3mf' || file.file_type === 'gcode' || file.file_type === 'stl' || file.file_type === 'gcode.3mf') && (
                 <button
                 <button
                   className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
                   className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 ${
                     hasPermission('library:read') ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'
                     hasPermission('library:read') ? 'text-white hover:bg-bambu-dark' : 'text-bambu-gray cursor-not-allowed'

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-Voj4BQlM.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Ci-vx1Gm.js"></script>
+    <script type="module" crossorigin src="/assets/index-Voj4BQlM.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-y4woBlMv.css">
     <link rel="stylesheet" crossorigin href="/assets/index-y4woBlMv.css">
   </head>
   </head>
   <body>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов