Эх сурвалжийг харах

feat: display 3D model dimensions in order lists and upload section

unknown 2 сар өмнө
parent
commit
735380725a

+ 20 - 0
backend/migrate_dimensions.py

@@ -0,0 +1,20 @@
+import db
+
+def migrate():
+    print("Migrating database to add dimensions column to order_files...")
+    try:
+        # Check if column exists first using MySQL syntax
+        columns = db.execute_query("SHOW COLUMNS FROM order_files")
+        column_names = [c['Field'] for c in columns]
+        
+        if 'dimensions' not in column_names:
+            db.execute_commit("ALTER TABLE order_files ADD COLUMN dimensions VARCHAR(255)")
+            print("Successfully added 'dimensions' column to 'order_files' table.")
+        else:
+            print("Column 'dimensions' already exists.")
+            
+    except Exception as e:
+        print(f"Migration error: {e}")
+
+if __name__ == "__main__":
+    migrate()

+ 8 - 4
backend/routers/files.py

@@ -32,13 +32,15 @@ async def upload_files(background_tasks: BackgroundTasks, files: List[UploadFile
         # --- CACHE CHECK (Hash based) ---
         filament_g = None
         print_time = None
+        dimensions = None
         cached_record = db.execute_query(
-            "SELECT filament_g, print_time FROM order_files WHERE file_hash = %s AND print_time IS NOT NULL LIMIT 1",
+            "SELECT filament_g, print_time, dimensions FROM order_files WHERE file_hash = %s AND print_time IS NOT NULL LIMIT 1",
             (file_hash,)
         )
         if cached_record:
             filament_g = cached_record[0]['filament_g']
             print_time = cached_record[0]['print_time']
+            dimensions = cached_record[0]['dimensions']
         
         # Only slice if not cached
         if not print_time and config.SYNC_SLICING_ON_UPLOAD and file_ext.lower() == ".stl":
@@ -47,6 +49,7 @@ async def upload_files(background_tasks: BackgroundTasks, files: List[UploadFile
             if result and result.get('success'):
                 filament_g = result.get('filament_g')
                 print_time = result.get('print_time_str')
+                dimensions = result.get('dimensions')
         
         preview_path = None
         db_preview_path = None
@@ -56,8 +59,8 @@ async def upload_files(background_tasks: BackgroundTasks, files: List[UploadFile
             db_preview_path = f"uploads/previews/{preview_filename}"
             preview_utils.generate_stl_preview(file_path, preview_path)
 
-        query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (NULL, %s, %s, %s, 1, %s, %s, %s, %s)"
-        f_id = db.execute_commit(query, (file.filename, db_file_path, file.size, file_hash, print_time, filament_g, db_preview_path))
+        query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path, dimensions) VALUES (NULL, %s, %s, %s, 1, %s, %s, %s, %s, %s)"
+        f_id = db.execute_commit(query, (file.filename, db_file_path, file.size, file_hash, print_time, filament_g, db_preview_path, dimensions))
         
         # If preview/slicing didn't happen (not cached and not sync), run in background
         if not print_time or not db_preview_path:
@@ -65,6 +68,7 @@ async def upload_files(background_tasks: BackgroundTasks, files: List[UploadFile
 
         uploaded_data.append({
             "id": f_id, "filename": file.filename, "size": file.size,
-            "print_time": print_time, "filament_g": filament_g, "preview_path": db_preview_path
+            "print_time": print_time, "filament_g": filament_g, "preview_path": db_preview_path,
+            "dimensions": dimensions
         })
     return {"uploaded": uploaded_data}

+ 7 - 4
backend/services/link_importer.py

@@ -73,20 +73,22 @@ class LinkImporter:
                     file_hash = hashlib.sha256(content).hexdigest()
                     file_size = len(content)
                     
-                    # Check cache for filament/time
+                    # Check cache for filament/time/dimensions
                     filament_g = None
                     print_time = None
+                    dimensions = None
                     cached = db.execute_query(
-                        "SELECT filament_g, print_time FROM order_files WHERE file_hash = %s AND print_time IS NOT NULL LIMIT 1",
+                        "SELECT filament_g, print_time, dimensions FROM order_files WHERE file_hash = %s AND print_time IS NOT NULL LIMIT 1",
                         (file_hash,)
                     )
                     if cached:
                         filament_g = cached[0]['filament_g']
                         print_time = cached[0]['print_time']
+                        dimensions = cached[0]['dimensions']
                     
                     # Insert into DB
-                    query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (NULL, %s, %s, %s, 1, %s, %s, %s, NULL)"
-                    f_id = db.execute_commit(query, (filename, db_file_path, file_size, file_hash, print_time, filament_g))
+                    query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path, dimensions) VALUES (NULL, %s, %s, %s, 1, %s, %s, %s, NULL, %s)"
+                    f_id = db.execute_commit(query, (filename, db_file_path, file_size, file_hash, print_time, filament_g, dimensions))
                     
                     # Trigger background processing (previews, etc.)
                     if background_tasks:
@@ -98,6 +100,7 @@ class LinkImporter:
                         "size": file_size,
                         "print_time": print_time,
                         "filament_g": filament_g,
+                        "dimensions": dimensions,
                         "status": "success"
                     })
         

+ 4 - 4
backend/services/order_processing.py

@@ -23,8 +23,8 @@ def process_order_slicing(order_id: int):
             result = slicer_utils.slice_model(file_path)
             if result and result.get('success'):
                 db.execute_commit(
-                    "UPDATE order_files SET print_time = %s, filament_g = %s WHERE id = %s",
-                    (result.get('print_time_str'), result.get('filament_g'), f['id'])
+                    "UPDATE order_files SET print_time = %s, filament_g = %s, dimensions = %s WHERE id = %s",
+                    (result.get('print_time_str'), result.get('filament_g'), result.get('dimensions'), f['id'])
                 )
 
 def process_single_file_async(file_id: int):
@@ -56,8 +56,8 @@ def process_single_file_async(file_id: int):
         result = slicer_utils.slice_model(file_path)
         if result and result.get('success'):
             db.execute_commit(
-                "UPDATE order_files SET print_time = %s, filament_g = %s WHERE id = %s",
-                (result.get('print_time_str'), result.get('filament_g'), file_id)
+                "UPDATE order_files SET print_time = %s, filament_g = %s, dimensions = %s WHERE id = %s",
+                (result.get('print_time_str'), result.get('filament_g'), result.get('dimensions'), file_id)
             )
             
     # Notify user/admin via WS if possible?

+ 13 - 3
backend/slicer_utils.py

@@ -27,9 +27,12 @@ def slice_model(file_path: str):
         process = subprocess.run(cmd, capture_output=True, text=True, check=True)
         content = process.stdout
         
-        # Regex to find volume in mm3
-        # Format: volume = 87552.437500
+        # Regex to find volume and sizes
         volume_match = re.search(r"volume\s*=\s*([\d\.]+)", content)
+        size_x_match = re.search(r"size_x\s*=\s*([\d\.]+)", content)
+        size_y_match = re.search(r"size_y\s*=\s*([\d\.]+)", content)
+        size_z_match = re.search(r"size_z\s*=\s*([\d\.]+)", content)
+
         if not volume_match:
             logger.error("Volume could not be parsed from PrusaSlicer output.")
             return None
@@ -37,11 +40,17 @@ def slice_model(file_path: str):
         volume_mm3 = float(volume_match.group(1))
         volume_cm3 = volume_mm3 / 1000.0
         
+        dimensions_str = ""
+        if size_x_match and size_y_match and size_z_match:
+            x = round(float(size_x_match.group(1)), 1)
+            y = round(float(size_y_match.group(1)), 1)
+            z = round(float(size_z_match.group(1)), 1)
+            dimensions_str = f"{x} x {y} x {z} mm"
+
         # Estimate weight assuming average density of PLA/PETG (~1.25 g/cm3)
         filament_g = round(volume_cm3 * 1.25, 2)
         
         # Estimate printing time assuming average volumetric flow rate of ~10 mm3/s
-        # (This is a very realistic average for standard 0.4mm nozzle including travel/supports)
         time_seconds = int(volume_mm3 / 10.0)
         
         hours = time_seconds // 3600
@@ -55,6 +64,7 @@ def slice_model(file_path: str):
         return {
             "filament_g": filament_g,
             "print_time_str": time_str,
+            "dimensions": dimensions_str,
             "success": True
         }
         

+ 17 - 1
src/components/ModelUploadSection.vue

@@ -227,6 +227,20 @@
                 <div v-if="file.isUploading" class="w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
               </div>
               <div class="flex items-center gap-2 text-sm text-muted-foreground">
+                <div v-if="file.printTime || file.filamentG || file.dimensions" class="flex flex-wrap gap-2 mt-2">
+                  <div v-if="file.dimensions" class="flex items-center gap-1 bg-primary/10 text-primary px-2 py-0.5 rounded-full text-[10px] font-bold">
+                    <div class="w-1 h-1 rounded-full bg-current" />
+                    {{ file.dimensions }}
+                  </div>
+                  <div v-if="file.printTime" class="flex items-center gap-1 bg-secondary text-foreground/60 px-2 py-0.5 rounded-full text-[10px] font-bold">
+                    <div class="w-1 h-1 rounded-full bg-current" />
+                    {{ file.printTime }}
+                  </div>
+                  <div v-if="file.filamentG" class="flex items-center gap-1 bg-secondary text-foreground/60 px-2 py-0.5 rounded-full text-[10px] font-bold">
+                    <div class="w-1 h-1 rounded-full bg-current" />
+                    {{ file.filamentG.toFixed(1) }}g
+                  </div>
+                </div>
                 <span>{{ file.type }} • {{ formatFileSize(file.size) }}</span>
                 <span v-if="file.basePrice" class="font-semibold text-primary ml-2 bg-primary/10 px-2 py-0.5 rounded-md">
                   ~{{ (file.basePrice * file.quantity).toFixed(2) }} EUR
@@ -338,7 +352,7 @@ import { defineAsyncComponent } from "vue";
 const StlViewer = defineAsyncComponent(() => import("@/components/StlViewer.vue"));
 import { submitOrder, getCurrentUser, getMaterials, getPriceEstimate, uploadFilesToServer, importFromUrl } from "@/lib/api";
 
-interface UploadedFile { id: string; dbId?: number; name: string; size: number; type: string; file?: File; quantity: number; basePrice?: number; isUploading?: boolean; printTime?: string; filamentG?: number; }
+interface UploadedFile { id: string; dbId?: number; name: string; size: number; type: string; file?: File; quantity: number; basePrice?: number; isUploading?: boolean; printTime?: string; filamentG?: number; dimensions?: string; }
 
 // --- CONFIGURATION ---
 // Set to false to hide the hardcore slicing data (time and weight) from the end-user
@@ -464,6 +478,7 @@ async function addFiles(rawFiles: File[]) {
           newFiles[idx].isUploading = false;
           if (u.print_time) newFiles[idx].printTime = u.print_time;
           if (u.filament_g) newFiles[idx].filamentG = u.filament_g;
+          if (u.dimensions) newFiles[idx].dimensions = u.dimensions;
         });
       }
     } catch (err) {
@@ -498,6 +513,7 @@ async function handleImportUrl() {
           quantity: 1,
           printTime: f.print_time,
           filamentG: f.filament_g,
+          dimensions: f.dimensions,
           isUploading: false
         });
       }

+ 2 - 1
src/components/admin/OrderCard.vue

@@ -138,7 +138,8 @@
                 <p class="text-[11px] font-bold truncate mb-1 pr-8">{{ f.filename }}</p>
                 <div class="flex flex-wrap gap-2 items-center">
                   <span v-if="f.file_size" class="text-[9px] text-muted-foreground uppercase opacity-60">{{ (f.file_size / 1024 / 1024).toFixed(1) }} MB</span>
-                  <div v-if="f.print_time || f.filament_g" class="flex gap-2 border-l border-border/50 pl-2">
+                  <div v-if="f.print_time || f.filament_g || f.dimensions" class="flex gap-2 border-l border-border/50 pl-2">
+                    <span v-if="f.dimensions" class="text-[9px] font-bold text-primary/80">📏 {{ f.dimensions }}</span>
                     <span v-if="f.print_time" class="text-[9px] font-bold text-primary/80">⏱️ {{ f.print_time }}</span>
                     <span v-if="f.filament_g" class="text-[9px] font-bold text-primary/80">⚖️ {{ f.filament_g.toFixed(1) }}g</span>
                   </div>

+ 2 - 1
src/pages/Orders.vue

@@ -169,7 +169,8 @@
                       <div class="flex items-center gap-2 mt-1">
                         <span v-if="file.quantity > 1" class="text-[8px] font-bold text-primary">x{{ file.quantity }}</span>
                       </div>
-                      <div v-if="file.print_time || file.filament_g" class="flex flex-col gap-0.5 mt-2 pt-2 border-t border-border/10">
+                      <div v-if="file.print_time || file.filament_g || file.dimensions" class="flex flex-col gap-0.5 mt-2 pt-2 border-t border-border/10">
+                        <span v-if="file.dimensions" class="text-[8px] text-primary/80">📏 {{ file.dimensions }}</span>
                         <span v-if="file.print_time" class="text-[8px] text-primary/80">⏱️ {{ file.print_time }}</span>
                         <span v-if="file.filament_g" class="text-[8px] text-primary/80">⚖️ {{ file.filament_g.toFixed(1) }}g</span>
                       </div>