feat: galeria muestra foto impresa + estimacion por placa en detalle
- Galeria: cards muestran imagen de referencia si existe, fallback a thumbnail 3D - Detalle: nueva seccion 'Placas de impresion' con estimacion individual por parte - Endpoint /estimate ahora acepta file_id para calcular por archivo - Visualizacion de placa 220x220mm con proporcion de pieza y totales acumulados
This commit is contained in:
@@ -171,6 +171,17 @@
|
||||
<div id="validation-result" class="hidden p-4 rounded-xl bg-slate-900/50 border border-white/5 text-sm space-y-1"></div>
|
||||
<div id="estimation-result" class="hidden p-4 rounded-xl bg-slate-900/50 border border-white/5 text-sm space-y-1"></div>
|
||||
|
||||
<!-- Plates Estimation -->
|
||||
<div class="glass rounded-2xl p-5 border border-white/5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-xs font-medium text-slate-500 uppercase tracking-wider block">Placas de impresion</label>
|
||||
<button onclick="runPlateEstimation()" class="px-3 py-1.5 rounded-lg bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30 text-xs font-medium transition-colors">
|
||||
Calcular placas
|
||||
</button>
|
||||
</div>
|
||||
<div id="plates-result" class="space-y-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 flex gap-3">
|
||||
<a id="btn-download" href="#" class="flex-1 px-5 py-3 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-bold text-sm shadow-lg shadow-cyan-500/20 transition-all hover:scale-[1.02] text-center flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
|
||||
|
||||
@@ -58,6 +58,13 @@ function renderGrid(models, append = false) {
|
||||
`<span class="px-1.5 py-0.5 rounded bg-cyan-500/10 text-cyan-400 text-[10px] font-medium">${t.name}</span>`
|
||||
).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 = `
|
||||
<div class="relative h-52 bg-slate-900 overflow-hidden">
|
||||
${selectionMode ? `
|
||||
@@ -67,7 +74,7 @@ function renderGrid(models, append = false) {
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<img src="/api/models/${m.id}/thumbnail" alt="${m.title}" loading="lazy" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" onerror="this.style.display='none'; this.parentElement.innerHTML='<div class=\'w-full h-full flex items-center justify-center text-slate-600\'><svg class=\'w-10 h-10\' fill=\'none\' stroke=\'currentColor\' viewBox=\'0 0 24 24\'><path stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'1.5\' d=\'M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4\'/></svg></div>';">
|
||||
<img src="${imgUrl}" alt="${m.title}" loading="lazy" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" onerror="this.onerror=null; this.src='/api/models/${m.id}/thumbnail';">
|
||||
<div class="absolute top-3 right-3">
|
||||
<span class="px-2.5 py-1 rounded-lg bg-black/50 backdrop-blur text-xs font-medium text-white border border-white/10">${m.faces ?? '?'} caras</span>
|
||||
</div>
|
||||
|
||||
@@ -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 = '<p class="text-sm text-slate-500">No hay partes para estimar.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<p class="text-sm text-slate-400">Calculando placas...</p>';
|
||||
|
||||
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 = `
|
||||
<div class="space-y-4">
|
||||
${results.map((r, i) => r.error ? `
|
||||
<div class="p-3 rounded-xl bg-red-500/10 border border-red-500/20 text-sm text-red-400">
|
||||
${r.part.filename}: Error al calcular
|
||||
</div>
|
||||
` : `
|
||||
<div class="p-4 rounded-xl bg-slate-900/50 border border-white/5">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium">${r.part_name}</span>
|
||||
<span class="text-[10px] px-2 py-0.5 rounded-full ${r.fitColor} text-white">${r.fitText}</span>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<div class="shrink-0">
|
||||
<div class="relative w-[200px] h-[200px] border border-slate-600 rounded bg-slate-800/50" style="background-image: linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); background-size: 18.18px 18.18px;">
|
||||
<div class="absolute rounded bg-cyan-500/30 border border-cyan-400/60 flex items-center justify-center text-[10px] text-cyan-300" style="width:${r.rectW}px; height:${r.rectH}px; left:4px; top:4px;">
|
||||
${r.width_mm}x${r.depth_mm}
|
||||
</div>
|
||||
<span class="absolute bottom-1 right-1 text-[9px] text-slate-500">220x220mm</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<div><span class="text-slate-500">Volumen:</span> <span class="text-slate-200">${r.volume_cm3} cm³</span></div>
|
||||
<div><span class="text-slate-500">Peso:</span> <span class="text-slate-200">${r.grams} g</span></div>
|
||||
<div><span class="text-slate-500">Alto:</span> <span class="text-slate-200">${r.height_mm} mm</span></div>
|
||||
<div><span class="text-slate-500">Costo:</span> <span class="text-cyan-400 font-bold">$${r.cost}</span></div>
|
||||
<div class="col-span-2"><span class="text-slate-500">Tiempo:</span> <span class="text-slate-200">${r.estimated_time}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<div class="p-4 rounded-xl bg-cyan-500/10 border border-cyan-500/20">
|
||||
<p class="text-sm font-bold text-cyan-400 mb-1">Total estimado (${results.length} placa${results.length > 1 ? 's' : ''})</p>
|
||||
<div class="grid grid-cols-3 gap-3 text-xs">
|
||||
<div><span class="text-slate-500">Peso:</span> <span class="text-slate-200">${totalGrams.toFixed(2)} g</span></div>
|
||||
<div><span class="text-slate-500">Costo:</span> <span class="text-cyan-400 font-bold">$${totalCost.toFixed(2)}</span></div>
|
||||
<div><span class="text-slate-500">Tiempo:</span> <span class="text-slate-200">${totalHours}h ${totalMins}m</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ====== COMPARISON ======
|
||||
async function openCompareModal() {
|
||||
const modal = document.getElementById('compare-modal');
|
||||
|
||||
Reference in New Issue
Block a user