Commit inicial del cotizador 3D

This commit is contained in:
ganglyglub2-gif
2025-11-07 11:20:45 -06:00
commit 39ad677f6b
12 changed files with 1256 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
<?php
/*
==========================================================
REFERENCIA DE CÁLCULOS ORIGINALES DE COSTOS POR IMPRESORA
==========================================================
Estos valores sirvieron de base para calcular los precios globales
usados en el archivo precios.json. Se mantienen aquí como referencia
para futuras actualizaciones de costos, sustituciones de impresoras
o materiales.
function obtenerDatosImpresoras() {
return [
'markforged' => [
'costo_impresora' => 99480.60, // $4,980 USD × 19.97
'vida_util_horas' => 36500,
'materiales' => [
'onyx' => [
'costo_por_cm3' => 6.37 // ($255 USD × 19.97) / 800 cm³
]
]
],
'elegoo' => [
'costo_impresora' => 15430.32, // $13,302 × 1.16 (IVA)
'vida_util_horas' => 2000,
'materiales' => [
'resina_transparente_flexible' => ['costo_por_cm3' => 1.60], // $1,600/L ÷ 1000 cm³/L
'resina_tipo_abs' => ['costo_por_cm3' => 0.50], // $500/L ÷ 1000
'resina_estandar' => ['costo_por_cm3' => 0.40] // $400/L ÷ 1000
]
],
'kingroon' => [
'costo_impresora' => 8120.00, // $7,000 × 1.16 (IVA)
'vida_util_horas' => 78000,
'materiales' => [
'pla' => ['costo_por_cm3' => 0.3125], // $250/kg ÷ 800 cm³/kg (aprox.)
'abs' => ['costo_por_cm3' => 0.30], // $300/kg ÷ 1000
'petg' => ['costo_por_cm3' => 0.30], // $300/kg ÷ 1000
'tpu' => ['costo_por_cm3' => 0.90] // $900/kg ÷ 1000
]
],
'creality' => [
'costo_impresora' => 23326.44, // $20,109 × 1.16 (IVA)
'vida_util_horas' => 2500,
'materiales' => [
'pla' => ['costo_por_cm3' => 0.25], // $250/kg ÷ 1000 cm³/kg
'abs' => ['costo_por_cm3' => 0.30], // $300/kg ÷ 1000
'petg' => ['costo_por_cm3' => 0.30], // $300/kg ÷ 1000
'tpu' => ['costo_por_cm3' => 0.90] // $900/kg ÷ 1000
]
]
];
}
💬 Nota:
Estos datos fueron usados para derivar los precios promedio por material
que ahora se encuentran en precios.json, los cuales son valores globales
por tipo de material independientemente de la impresora.
==========================================================
*/
function calcularCotizacionImpresion($volumen, $tiempoHoras, $materialSeleccionado, $impresoraSeleccionada) {
$impresorasPath = __DIR__ . '/impresoras.json';
$preciosPath = __DIR__ . '/precios.json';
$datosImpresoras = json_decode(file_get_contents($impresorasPath), true);
$datosPrecios = json_decode(file_get_contents($preciosPath), true);
if (!$datosImpresoras || !$datosPrecios) {
return calcularCotizacionDefault();
}
$impresora = $datosImpresoras[$impresoraSeleccionada] ?? null;
if (!$impresora) return calcularCotizacionDefault();
$vidaUtil = $impresora['vida_util_horas'] ?? 2000;
$costoImpresora = $impresora['costo_impresora'] ?? 10000;
$precioPorCm3 = $datosPrecios['materiales'][$materialSeleccionado] ?? 0.25;
$margenConsumible = $datosPrecios['margenes']['consumible'] ?? 0.015;
$margenOperacion = $datosPrecios['margenes']['operacion'] ?? 0.02;
$margenIndirectos = $datosPrecios['margenes']['indirectos'] ?? 0.30;
// Cálculos
$costoMaterial = $volumen * $precioPorCm3;
$costoTiempo = $tiempoHoras * ($costoImpresora / $vidaUtil);
$subtotalProduccion = $costoMaterial + $costoTiempo;
$consumible = $subtotalProduccion * $margenConsumible;
$operacion = $subtotalProduccion * $margenOperacion;
$subtotalOperacion = $consumible + $operacion;
$costoFinal = $subtotalProduccion + $subtotalOperacion;
$indirectos = $costoFinal * $margenIndirectos;
$precioFinal = $costoFinal + $indirectos;
return [
'costo_material' => round($costoMaterial, 2),
'costo_tiempo' => round($costoTiempo, 2),
'subtotal_produccion' => round($subtotalProduccion, 2),
'consumible_impresion' => round($consumible, 2),
'costo_operacion' => round($operacion, 2),
'subtotal_operacion' => round($subtotalOperacion, 2),
'costo_final_impresion' => round($costoFinal, 2),
'indirectos' => round($indirectos, 2),
'precio_final' => round($precioFinal, 2)
];
}
function calcularCotizacionDefault() {
return [
'costo_material' => 0,
'costo_tiempo' => 0,
'subtotal_produccion' => 0,
'consumible_impresion' => 0,
'costo_operacion' => 0,
'subtotal_operacion' => 0,
'costo_final_impresion' => 0,
'indirectos' => 0,
'precio_final' => 0
];
}
?>
+275
View File
@@ -0,0 +1,275 @@
/* ==========================
ESTILO BASE ORIGINAL
========================== */
body {
font-family: "Poppins", sans-serif;
background: linear-gradient(135deg, #2c3e50, #4ca1af);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
/* CONTENEDOR PRINCIPAL */
.contenedor {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 30px 40px;
width: 900px;
max-width: 95%;
text-align: center;
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
h1 {
margin-bottom: 20px;
font-size: 1.6em;
}
/* CAMPOS DEL FORMULARIO */
.campo {
margin-bottom: 15px;
text-align: left;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
}
/* DROPDOWNS MÁS COMPACTOS */
select {
width: 100%;
padding: 8px 10px;
border-radius: 8px;
border: none;
outline: none;
font-size: 0.9em;
height: 40px;
}
input[type="file"] {
width: 100%;
padding: 10px;
border-radius: 8px;
border: none;
outline: none;
}
.boton {
margin-top: 15px;
width: 60%;
background-color: #00b894;
border: none;
color: white;
font-weight: bold;
padding: 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.3s;
font-size: 0.95em;
margin-left: auto;
margin-right: auto;
display: block;
}
.boton:hover {
background-color: #019875;
}
/* ==========================
RESULTADOS
========================== */
.resultado {
margin-top: 25px;
width: 100%;
}
/* TARJETAS DE RESULTADOS */
.card-resultado {
background: linear-gradient(145deg, #00b894, #019875);
color: #fff;
border-radius: 14px;
padding: 25px;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
animation: aparecer 0.6s ease;
height: fit-content;
}
.card-resultado h2,
.card-resultado h3 {
text-align: center;
margin-bottom: 10px;
font-weight: bold;
}
.res-item {
margin: 6px 0;
font-size: 1.05em;
}
.card-resultado hr {
border: none;
border-top: 1px solid rgba(255,255,255,0.4);
margin: 15px 0;
}
.total {
font-size: 1.2em;
text-align: center;
font-weight: bold;
margin-top: 10px;
}
.card-resultado.error {
background: linear-gradient(145deg, #ff7675, #d63031);
}
/* BOTÓN DESCARGAR */
.acciones {
text-align: center;
margin-top: 15px;
}
.boton-descargar {
display: inline-block;
background-color: #0984e3;
color: #fff;
padding: 10px 16px;
border-radius: 8px;
text-decoration: none;
transition: background 0.3s;
font-weight: 600;
}
.boton-descargar:hover {
background-color: #74b9ff;
}
/* ==========================
DOS COLUMNAS LATERALES
========================== */
.resultado-dos-columnas {
display: flex;
justify-content: space-between;
gap: 20px;
margin-top: 25px;
width: 100%;
}
.resultado-dos-columnas .card-resultado {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
/* ==========================
ANIMACIÓN
========================== */
@keyframes aparecer {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ==========================
ADAPTABILIDAD MÓVIL
========================== */
@media (max-width: 768px) {
body {
height: auto;
padding: 20px;
}
.contenedor {
width: 100%;
max-width: 420px;
padding: 20px;
}
.resultado-dos-columnas {
flex-direction: column;
}
.resultado-dos-columnas .card-resultado {
flex: 1 1 100%;
}
.boton {
width: 80%;
}
}
/* =============================
🔵 Loader (Barra y Spinner)
============================= */
#loader-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 9999;
justify-content: center;
align-items: center;
flex-direction: column;
color: #fff;
font-family: Arial, sans-serif;
text-align: center;
opacity: 0;
transition: opacity 0.4s ease;
}
#loader-overlay[style*="display: flex"] {
opacity: 1;
}
.loader-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loader-spinner {
width: 80px;
height: 80px;
border: 6px solid rgba(255, 255, 255, 0.2);
border-top-color: #00b4d8;
border-radius: 50%;
animation: girar 1s linear infinite;
}
.loader-text {
margin-top: 20px;
font-size: 18px;
font-weight: 500;
text-shadow: 0 0 6px rgba(0, 180, 216, 0.8);
}
.barra-contenedor {
width: 80%;
max-width: 400px;
height: 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 6px;
overflow: hidden;
margin-top: 20px;
}
.barra-progreso {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #00b4d8, #48cae4, #90e0ef);
transition: width 0.2s ease-in-out;
}
@keyframes girar {
to { transform: rotate(360deg); }
}
+43
View File
@@ -0,0 +1,43 @@
{
"markforged": {
"nombre": "Markforged",
"vida_util_horas": 36500,
"costo_impresora": 99480.60,
"materiales": {
"onyx": "Onyx"
}
},
"elegoo": {
"nombre": "Elegoo",
"vida_util_horas": 2000,
"costo_impresora": 15430.32,
"materiales": {
"resina_transparente_flexible": "Resina transparente flexible",
"resina_tipo_abs": "Resina tipo ABS",
"resina_estandar": "Resina estándar"
}
},
"kingroon": {
"nombre": "Kingroon",
"vida_util_horas": 78000,
"costo_impresora": 8120,
"materiales": {
"pla": "PLA",
"abs": "ABS",
"petg": "PETG",
"tpu": "TPU"
}
},
"creality": {
"nombre": "Creality",
"vida_util_horas": 2500,
"costo_impresora": 23326.44,
"materiales": {
"pla": "PLA",
"abs": "ABS",
"petg": "PETG",
"tpu": "TPU"
}
}
}
+283
View File
@@ -0,0 +1,283 @@
<?php session_start(); ?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Cotizador 3D</title>
<link rel="stylesheet" href="estilos.css">
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/STLLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
<script>
let datosImpresoras = {};
async function cargarImpresoras() {
const res = await fetch('impresoras.json');
datosImpresoras = await res.json();
const selectMarca = document.getElementById('marca');
selectMarca.innerHTML = '';
for (const [clave, data] of Object.entries(datosImpresoras)) {
const option = document.createElement('option');
option.value = clave;
option.textContent = data.nombre;
selectMarca.appendChild(option);
}
actualizarMateriales();
}
function actualizarMateriales() {
const marca = document.getElementById('marca').value;
const select = document.getElementById('material');
select.innerHTML = '';
if (!datosImpresoras[marca]) return;
const materiales = datosImpresoras[marca].materiales;
for (const [clave, nombre] of Object.entries(materiales)) {
const option = document.createElement('option');
option.value = clave;
option.textContent = nombre;
select.appendChild(option);
}
}
function mostrarLoader() {
const overlay = document.getElementById('loader-overlay');
const barra = document.querySelector('.barra-progreso');
const texto = document.querySelector('.loader-text');
overlay.style.display = 'flex';
barra.style.width = '0%';
texto.textContent = 'Procesando archivo...';
let progresoSimulado = 0;
let velocidad = 0.5 + Math.random() * 1;
let intervalo = setInterval(() => {
if (progresoSimulado < 97) {
progresoSimulado += velocidad;
barra.style.width = progresoSimulado + '%';
}
}, 30);
return () => {
clearInterval(intervalo);
let progresoFinal = progresoSimulado;
const animarFinal = setInterval(() => {
progresoFinal += 1;
if (progresoFinal >= 100) {
progresoFinal = 100;
clearInterval(animarFinal);
texto.textContent = 'Completado';
setTimeout(() => overlay.style.display = 'none', 400);
}
barra.style.width = progresoFinal + '%';
}, 25);
};
}
// Variables para el teclado y el modelo
let mesh;
const keyState = {};
// Listeners para el teclado
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key === 'a' || key === 'arrowleft') {
keyState['a'] = true;
}
if (key === 'd' || key === 'arrowright') {
keyState['d'] = true;
}
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (key === 'a' || key === 'arrowleft') {
keyState['a'] = false;
}
if (key === 'd' || key === 'arrowright') {
keyState['d'] = false;
}
});
// --- Función del visualizador 3D ---
function cargarVisualizador(stlUrl) {
const modelContainer = document.getElementById("model");
if (!modelContainer) {
console.error("El contenedor #model no se encontró.");
return;
}
// Limpiar contenedor
while (modelContainer.firstChild) {
modelContainer.removeChild(modelContainer.firstChild);
}
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, modelContainer.clientWidth / 500, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(modelContainer.clientWidth, 500);
modelContainer.appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
// Controles del Mouse
controls.enablePan = true;
controls.enableZoom = true;
controls.enableDamping = true;
controls.dampingFactor = 0.05;
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1.2);
scene.add(hemiLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
const loader = new THREE.STLLoader();
loader.load(stlUrl, function (geometry) {
const material = new THREE.MeshPhongMaterial({ color: 0x0077ff });
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Centrar modelo
const box = new THREE.Box3().setFromObject(mesh);
const center = box.getCenter(new THREE.Vector3());
mesh.position.sub(center);
// --- Ajuste de la cámara ---
const size = box.getSize(new THREE.Vector3()).length();
camera.position.x = 0;
camera.position.z = size * 1.5;
camera.position.y = size * 0.4;
controls.target.set(0, 0, 0);
controls.update();
// Iniciar la animación
animate();
}, undefined, function (error) {
console.error("Error al cargar el STL:", error);
modelContainer.innerHTML = "<p style='color:red; padding: 20px;'>❌ No se pudo cargar el modelo 3D.</p>";
});
// --- Bucle de Animación ---
function animate() {
requestAnimationFrame(animate);
// Lógica de Teclado
if (mesh) {
const rotationSpeed = 0.02;
if (keyState['a']) {
mesh.rotation.z -= rotationSpeed;
}
if (keyState['d']) {
mesh.rotation.z += rotationSpeed;
}
}
controls.update();
renderer.render(scene, camera);
}
// Ajustar el tamaño si el contenedor cambia
function onWindowResize() {
if (modelContainer) {
const width = modelContainer.clientWidth;
const height = 500; // Altura fija
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
}
new ResizeObserver(onWindowResize).observe(modelContainer);
onWindowResize(); // Llamada inicial
}
window.addEventListener('DOMContentLoaded', () => {
cargarImpresoras();
document.getElementById('marca').addEventListener('change', actualizarMateriales);
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
e.preventDefault();
const finalizarLoader = mostrarLoader();
const formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(html => {
finalizarLoader();
setTimeout(() => {
document.querySelector('.resultado').innerHTML = html;
const stlData = document.getElementById('stl-data-url');
if (stlData && stlData.dataset.url) {
cargarVisualizador(stlData.dataset.url);
} else {
console.error("No se encontró la URL del STL en la respuesta.");
const modelContainer = document.getElementById("model");
if(modelContainer) modelContainer.innerHTML = "<p style='color:red; padding: 20px;'>Error: No se recibió la ruta del modelo 3D.</p>";
}
}, 500);
})
.catch(() => {
alert('Error al procesar el archivo.');
document.getElementById('loader-overlay').style.display = 'none';
});
});
});
</script>
</head>
<body>
<div class="contenedor">
<h1>Cotizador de Impresión 3D</h1>
<form action="procesar.php" method="POST" enctype="multipart/form-data" class="formulario">
<div class="campo">
<label for="marca">Selecciona la impresora:</label>
<select name="marca" id="marca" required></select>
</div>
<div class="campo">
<label for="material">Selecciona el material:</label>
<select name="material" id="material" required></select>
</div>
<div class="campo">
<label for="stl_file">Sube tu archivo STL:</label>
<input type="file" name="stl_file" id="stl_file" accept=".stl" required>
</div>
<button type="submit" class="boton">Calcular cotización</button>
</form>
<div class="resultado"></div>
</div>
<div id="loader-overlay">
<div class="loader-container">
<div class="loader-spinner"></div>
<p class="loader-text">Procesando archivo...</p>
<div class="barra-contenedor">
<div class="barra-progreso"></div>
</div>
</div>
</div>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
{
"materiales": {
"pla": 0.3125,
"abs": 0.30,
"petg": 0.30,
"tpu": 0.90,
"onyx": 6.37,
"resina_transparente_flexible": 1.60,
"resina_tipo_abs": 0.50,
"resina_estandar": 0.40
},
"margenes": {
"consumible": 0.015,
"operacion": 0.02,
"indirectos": 0.30
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['stl_file']) && $_FILES['stl_file']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . "/uploads/";
$stlPath = $uploadDir . basename($_FILES['stl_file']['name']);
$gcodePath = $uploadDir . pathinfo($_FILES['stl_file']['name'], PATHINFO_FILENAME) . ".gcode";
if (!move_uploaded_file($_FILES['stl_file']['tmp_name'], $stlPath)) {
echo '<div class="card-resultado error">❌ Error al mover el archivo.</div>';
exit;
}
// Ejecutar el slicer (PrusaSlicer o script JS)
$cmd = "node /var/www/html/runPrusa.js " . escapeshellarg($stlPath);
exec($cmd, $output, $return_var);
if ($return_var !== 0 || empty($output)) {
echo '<div class="card-resultado error">⚠️ Error al generar G-code:<br>' . implode("<br>", $output) . '</div>';
exit;
}
$result = json_decode($output[0], true);
if (!$result) {
echo '<div class="card-resultado error">⚠️ No se pudo leer la información del G-code.</div>';
exit;
}
// Datos del archivo
$margen = 0.10;
$filamento_mm = $result['filamentUsedMm'] ?? 0;
$filamento_cm3 = $result['filamentUsedCm3'] ?? 0;
$tiempo_estimado = $result['estimatedTime'] ?? "0h 0m 0s";
$filamento_mm_seg = $filamento_mm * (1 + $margen);
$filamento_cm3_seg = $filamento_cm3 * (1 + $margen);
$tiempo_seg = ajustarTiempo($tiempo_estimado, $margen);
$impresoraSeleccionada = $_POST['marca'] ?? 'markforged';
$materialSeleccionado = $_POST['material'] ?? 'pla';
include 'calculo_precio.php';
$cotizacion = calcularCotizacionImpresion(
$filamento_cm3_seg,
convertirTiempoAHoras($tiempo_seg),
$materialSeleccionado,
$impresoraSeleccionada
);
// --- MOSTRAR RESULTADOS ---
echo '
<div class="resultado-dos-columnas">
<div class="card-resultado">
<h3>📊 Datos Base</h3>
<div class="res-item"><strong>Filamento usado:</strong> ' . number_format($filamento_mm, 2) . ' mm</div>
<div class="res-item"><strong>Volumen usado:</strong> ' . number_format($filamento_cm3, 2) . ' cm³</div>
<div class="res-item"><strong>Tiempo estimado:</strong> ' . htmlspecialchars($tiempo_estimado) . '</div>
<hr>
<h3>🛡️ Con Margen de Seguridad (10%)</h3>
<div class="res-item"><strong>Filamento usado (seguridad):</strong> ' . number_format($filamento_mm_seg, 2) . ' mm</div>
<div class="res-item"><strong>Volumen usado (seguridad):</strong> ' . number_format($filamento_cm3_seg, 2) . ' cm³</div>
<div class="res-item"><strong>Tiempo estimado (seguridad):</strong> ' . htmlspecialchars($tiempo_seg) . '</div>
</div>
<div class="card-resultado">
<h3>💰 Cotización Estimada</h3>
<div class="res-item"><strong>Impresora:</strong> ' . htmlspecialchars(ucfirst($impresoraSeleccionada)) . '</div>
<div class="res-item"><strong>Material:</strong> ' . htmlspecialchars(ucfirst($materialSeleccionado)) . '</div>
<hr>
<div class="res-item">• Costo Material: <strong>' . number_format($cotizacion['costo_material'], 2) . ' MXN</strong></div>
<div class="res-item">• Costo Tiempo: <strong>' . number_format($cotizacion['costo_tiempo'], 2) . ' MXN</strong></div>
<div class="res-item">• Subtotal Producción: ' . number_format($cotizacion['subtotal_produccion'], 2) . ' MXN</div>
<div class="res-item">• Consumible Impresión: ' . number_format($cotizacion['consumible_impresion'], 2) . ' MXN</div>
<div class="res-item">• Costo Operación: ' . number_format($cotizacion['costo_operacion'], 2) . ' MXN</div>
<div class="res-item">• Subtotal Operación: ' . number_format($cotizacion['subtotal_operacion'], 2) . ' MXN</div>
<div class="res-item">• Costo Final: ' . number_format($cotizacion['costo_final_impresion'], 2) . ' MXN</div>
<div class="res-item">• Indirectos: ' . number_format($cotizacion['indirectos'], 2) . ' MXN</div>
<hr>
<div class="total">💵 Precio Final Estimado: ' . number_format($cotizacion['precio_final'], 2) . ' MXN</div>
<div class="acciones">
<a href="uploads/' . htmlspecialchars(basename($gcodePath)) . '" class="boton-descargar" download>⬇️ Descargar G-code</a>
</div>
</div>
</div>';
// === VISUALIZADOR 3D ===
echo '
<div id="model" style="width: 100%; height: 500px; margin-top: 20px; background: #111;"></div>
<div class="controles-info" style="width: 100%; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; margin-top: 10px; text-align: left; font-size: 0.9em; line-height: 1.6;">
<h4 style="margin: 0 0 10px 0; text-align: center; font-size: 1.1em;"> Controles del Visor 3D</h4>
<ul style="margin: 0; padding-left: 20px; list-style-type: disc;">
<li><strong>Girar Modelo (Orbitar):</strong> Clic Izquierdo + Arrastrar</li>
<li><strong>Mover Cámara (Pan):</strong> Clic Derecho + Arrastrar</li>
<li><strong>Acercar/Alejar (Zoom):</strong> Rueda del Mouse (Scroll)</li>
<li><strong>Rotar Pieza:</strong> Teclas A / D (o Flecha Izquierda / Derecha)</li>
</ul>
</div>
';
$stlUrl = 'uploads/' . htmlspecialchars(basename($stlPath));
echo '<div id="stl-data-url" data-url="' . $stlUrl . '" style="display: none;"></div>';
} else {
echo '<div class="card-resultado error">❌ No se recibió un archivo STL válido.</div>';
}
} else {
echo '<div class="card-resultado error">⚠️ Método no permitido.</div>';
}
// --- FUNCIONES AUXILIARES ---
function ajustarTiempo($tiempo_str, $porcentaje) {
preg_match_all("/(\\d+)([hms])/", $tiempo_str, $matches, PREG_SET_ORDER);
$segundos = 0;
foreach ($matches as $match) {
$valor = (int)$match[1];
switch ($match[2]) {
case "h": $segundos += $valor * 3600; break;
case "m": $segundos += $valor * 60; break;
case "s": $segundos += $valor; break;
}
}
$segundos_ajustados = round($segundos * (1 + $porcentaje));
$horas = intdiv($segundos_ajustados, 3600);
$minutos = intdiv($segundos_ajustados % 3600, 60);
$segundos_final = $segundos_ajustados % 60;
return "{$horas}h {$minutos}m {$segundos_final}s";
}
function convertirTiempoAHoras($tiempo_str) {
preg_match_all("/(\\d+)([hms])/", $tiempo_str, $matches, PREG_SET_ORDER);
$horas = 0;
foreach ($matches as $match) {
$valor = (int)$match[1];
switch ($match[2]) {
case "h": $horas += $valor; break;
case "m": $horas += $valor / 60; break;
case "s": $horas += $valor / 3600; break;
}
}
return $horas;
}
?>
+44
View File
@@ -0,0 +1,44 @@
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const stlFile = process.argv[2];
if (!stlFile || !fs.existsSync(stlFile)) {
console.error(JSON.stringify({ error: 'Archivo STL no encontrado' }));
process.exit(1);
}
const gcodeFile = stlFile.replace(/\.stl$/i, '.gcode');
// Ruta al perfil de PrusaSlicer
const profilePath = '/app/profiles/Ender3_Bltouch.ini';
// Ruta absoluta del binario
const prusaCmd = `/app/squashfs-root/squashfs-root/usr/bin/prusa-slicer --export-gcode -o ${gcodeFile} --load ${profilePath} ${stlFile}`;
exec(prusaCmd, (error, stdout, stderr) => {
if (error) {
console.error(JSON.stringify({ error: `Error al generar G-code: ${error.message}`, stderr }));
return;
}
if (!fs.existsSync(gcodeFile)) {
console.error(JSON.stringify({ error: 'No se generó el archivo G-code' }));
return;
}
const gcodeContent = fs.readFileSync(gcodeFile, 'utf-8');
const filamentMatchMm = gcodeContent.match(/filament used \[mm\] = ([0-9.]+)/i);
const filamentMatchCm3 = gcodeContent.match(/filament used \[cm3\] = ([0-9.]+)/i);
const timeMatch = gcodeContent.match(/estimated printing time .* = ([0-9hms ]+)/i);
const result = {
gcodeFile,
filamentUsedMm: filamentMatchMm ? parseFloat(filamentMatchMm[1]) : null,
filamentUsedCm3: filamentMatchCm3 ? parseFloat(filamentMatchCm3[1]) : null,
estimatedTime: timeMatch ? timeMatch[1].trim() : null
};
console.log(JSON.stringify(result));
});