diff --git a/app/routers/models.py b/app/routers/models.py index 3f1d568..40a9c75 100644 --- a/app/routers/models.py +++ b/app/routers/models.py @@ -560,18 +560,22 @@ def validate_mesh(model_id: int, db: Session = Depends(get_db)): @router.get("/{model_id}/estimate") -def estimate_print(model_id: int, price_per_kg: float = Query(20.0), material_density: float = Query(1.24), db: Session = Depends(get_db)): +def estimate_print(model_id: int, file_id: Optional[int] = Query(None), price_per_kg: float = Query(20.0), material_density: float = Query(1.24), db: Session = Depends(get_db)): model = db.query(Model3D).filter(Model3D.id == model_id).first() if not model: raise HTTPException(status_code=404, detail="Model not found") - primary = next((f for f in model.files if f.is_primary and f.file_type in ('stl', '3mf')), None) - if not primary: + if file_id: + target = next((f for f in model.files if f.id == file_id and f.file_type in ('stl', '3mf')), None) + else: + target = next((f for f in model.files if f.is_primary and f.file_type in ('stl', '3mf')), None) + + if not target: raise HTTPException(status_code=404, detail="No 3D file found") try: import trimesh - mesh = trimesh.load(primary.file_path, force='mesh') + mesh = trimesh.load(target.file_path, force='mesh') volume_cm3 = abs(float(mesh.volume)) / 1000.0 grams = volume_cm3 * material_density cost = (grams / 1000.0) * price_per_kg @@ -579,7 +583,11 @@ def estimate_print(model_id: int, price_per_kg: float = Query(20.0), material_de hours = seconds // 3600 mins = (seconds % 3600) // 60 + bounds = mesh.bounds + dims = bounds[1] - bounds[0] return { + "file_id": target.id, + "part_name": target.part_name or target.filename, "volume_cm3": round(volume_cm3, 2), "grams": round(grams, 2), "cost": round(cost, 2), @@ -587,6 +595,9 @@ def estimate_print(model_id: int, price_per_kg: float = Query(20.0), material_de "estimated_seconds": seconds, "price_per_kg": price_per_kg, "material_density": material_density, + "width_mm": round(float(dims[0]), 1), + "depth_mm": round(float(dims[1]), 1), + "height_mm": round(float(dims[2]), 1), } except Exception as e: raise HTTPException(status_code=500, detail=f"Estimation failed: {str(e)}") diff --git a/static/detail.html b/static/detail.html index 16dce5e..3742151 100644 --- a/static/detail.html +++ b/static/detail.html @@ -171,6 +171,17 @@ + +
+
+ + +
+
+
+
diff --git a/static/js/app.js b/static/js/app.js index a9a937b..96cc172 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -58,6 +58,13 @@ function renderGrid(models, append = false) { `${t.name}` ).join(''); + // Use reference image if available, fallback to 3D thumbnail + const refImage = (m.files || []).find(f => f.file_type === 'image'); + const imgUrl = refImage + ? `/images/${encodeURIComponent(refImage.filename)}` + : `/api/models/${m.id}/thumbnail`; + const imgLabel = refImage ? 'Foto' : 'Vista 3D'; + card.innerHTML = `
${selectionMode ? ` @@ -67,7 +74,7 @@ function renderGrid(models, append = false) {
` : ''} - ${m.title} + ${m.title}
${m.faces ?? '?'} caras
diff --git a/static/js/detail.js b/static/js/detail.js index b2d7372..daebf6f 100644 --- a/static/js/detail.js +++ b/static/js/detail.js @@ -900,6 +900,101 @@ async function runEstimation() { } } +// ====== PLATE ESTIMATION ====== +async function runPlateEstimation() { + const container = document.getElementById('plates-result'); + if (!container || !currentModel) return; + const parts = (currentModel.files || []).filter(f => f.file_type === 'stl' || f.file_type === '3mf'); + if (parts.length === 0) { + container.innerHTML = '

No hay partes para estimar.

'; + return; + } + + container.innerHTML = '

Calculando placas...

'; + + const PLATE_SIZE = 220; // mm (standard bed size) + let totalCost = 0; + let totalTime = 0; + let totalGrams = 0; + + const results = []; + for (const part of parts) { + try { + const data = await apiGet(`/models/${getModelId()}/estimate?file_id=${part.id}`); + totalCost += data.cost; + totalTime += data.estimated_seconds; + totalGrams += data.grams; + + // Determine fit color + const maxDim = Math.max(data.width_mm || 0, data.depth_mm || 0); + const fitColor = maxDim > PLATE_SIZE ? 'bg-red-500' : maxDim > PLATE_SIZE * 0.8 ? 'bg-yellow-500' : 'bg-cyan-500'; + const fitText = maxDim > PLATE_SIZE ? 'No cabe en placa estandar' : 'Cabe en placa'; + + // Scale for visual (200px = 220mm) + const scale = 200 / PLATE_SIZE; + const rectW = Math.min((data.width_mm || 10) * scale, 200); + const rectH = Math.min((data.depth_mm || 10) * scale, 200); + + results.push({ + ...data, + part, + fitColor, + fitText, + rectW, + rectH, + }); + } catch (err) { + results.push({ part, error: err.message }); + } + } + + const totalHours = Math.floor(totalTime / 3600); + const totalMins = Math.floor((totalTime % 3600) / 60); + + container.innerHTML = ` +
+ ${results.map((r, i) => r.error ? ` +
+ ${r.part.filename}: Error al calcular +
+ ` : ` +
+
+ ${r.part_name} + ${r.fitText} +
+
+
+
+
+ ${r.width_mm}x${r.depth_mm} +
+ 220x220mm +
+
+
+
Volumen: ${r.volume_cm3} cm³
+
Peso: ${r.grams} g
+
Alto: ${r.height_mm} mm
+
Costo: $${r.cost}
+
Tiempo: ${r.estimated_time}
+
+
+
+ `).join('')} + +
+

Total estimado (${results.length} placa${results.length > 1 ? 's' : ''})

+
+
Peso: ${totalGrams.toFixed(2)} g
+
Costo: $${totalCost.toFixed(2)}
+
Tiempo: ${totalHours}h ${totalMins}m
+
+
+
+ `; +} + // ====== COMPARISON ====== async function openCompareModal() { const modal = document.getElementById('compare-modal');