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
+6
View File
@@ -0,0 +1,6 @@
# Ignorar la carpeta de archivos subidos por el usuario
/www/uploads/
# Ignorar archivos de sistema operativo (opcional pero recomendado)
.DS_Store
Thumbs.db
+43
View File
@@ -0,0 +1,43 @@
# Base PHP con Apache
FROM php:8.2-apache
# Instalar Node.js y dependencias necesarias (incluye GTK para PrusaSlicer)
RUN apt-get update && apt-get install -y \
wget \
fuse \
libglu1-mesa \
libtbb-dev \
libdbus-1-3 \
nodejs \
npm \
unzip \
libgtk-3-0 \
libx11-xcb1 \
libxtst6 \
libxrandr2 \
libasound2 \
libpangocairo-1.0-0 \
libpango-1.0-0 \
libcairo2 \
libatk1.0-0 \
&& rm -rf /var/lib/apt/lists/*
# Copiar la carpeta www al contenedor
COPY ./www /var/www/html
# Crear carpeta para perfiles de impresora
RUN mkdir -p /app/profiles
# Descargar y descomprimir PrusaSlicer
RUN wget https://github.com/prusa3d/PrusaSlicer/releases/download/version_2.7.4/PrusaSlicer-2.7.4+linux-x64-GTK3-202404050928.AppImage -O /tmp/prusaslicer.AppImage \
&& chmod +x /tmp/prusaslicer.AppImage \
&& mkdir -p /app/squashfs-root \
&& /tmp/prusaslicer.AppImage --appimage-extract \
&& mv squashfs-root /app/squashfs-root \
&& rm /tmp/prusaslicer.AppImage
# Establecer directorio de trabajo
WORKDIR /var/www/html
# Exponer el puerto 80
EXPOSE 80
+12
View File
@@ -0,0 +1,12 @@
services:
web:
build: .
container_name: printer3d
ports:
- "8080:80"
volumes:
- ./www:/var/www/html
- ./profiles:/app/profiles
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini # <-- Agregado para subir archivos grandes
stdin_open: true
tty: true
+255
View File
@@ -0,0 +1,255 @@
# generated by PrusaSlicer 2.3.3+win64 on 2021-08-26 at 23:16:45 UTC
avoid_crossing_perimeters = 0
avoid_crossing_perimeters_max_detour = 0
bed_custom_model =
bed_custom_texture =
bed_shape = 3x3,228x3,228x228,3x228
bed_temperature = 60
before_layer_gcode = ;BEFORE_LAYER_CHANGE\nG92 E0\n;{layer_z}\n\n
between_objects_gcode =
bottom_fill_pattern = monotonic
bottom_solid_layers = 4
bottom_solid_min_thickness = 0
bridge_acceleration = 250
bridge_angle = 0
bridge_fan_speed = 100
bridge_flow_ratio = 0.95
bridge_speed = 25
brim_width = 0
clip_multipart_objects = 1
color_change_gcode = M600
colorprint_heights =
compatible_printers_condition_cummulative = "printer_model=~/(ENDER|CR|SERMOON).*/ and nozzle_diameter[0]==0.4";
complete_objects = 0
cooling = 1
cooling_tube_length = 5
cooling_tube_retraction = 91.5
default_acceleration = 500
default_filament_profile = "Generic PLA @CREALITY"
default_print_profile = 0.16mm OPTIMAL @CREALITY
deretract_speed = 40
disable_fan_first_layers = 3
dont_support_bridges = 1
draft_shield = 0
duplicate_distance = 6
elefant_foot_compensation = 0.1
end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n"
end_gcode = {if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+2, max_print_height)} F600 ; Move print head up{endif}\nG1 X5 Y{print_bed_max[1]*0.8} F{travel_speed*60} ; present print\n{if max_layer_z < max_print_height-10}G1 Z{z_offset+min(max_layer_z+70, max_print_height-10)} F600 ; Move print head further up{endif}\n{if max_layer_z < max_print_height*0.6}G1 Z{max_print_height*0.6} F600 ; Move print head further up{endif}\nM140 S0 ; turn off heatbed\nM104 S0 ; turn off temperature\nM107 ; turn off fan\nM84 X Y E ; disable motors
ensure_vertical_shell_thickness = 1
external_perimeter_extrusion_width = 0.45
external_perimeter_speed = 35
external_perimeters_first = 0
extra_loading_move = -2
extra_perimeters = 0
extruder_clearance_height = 34
extruder_clearance_radius = 47
extruder_colour = #CC80B5
extruder_offset = 0x0
extrusion_axis = E
extrusion_multiplier = 1
extrusion_width = 0.45
fan_always_on = 1
fan_below_layer_time = 60
filament_colour = #29B2B2
filament_cooling_final_speed = 3.4
filament_cooling_initial_speed = 2.2
filament_cooling_moves = 4
filament_cost = 0
filament_density = 0
filament_deretract_speed = nil
filament_diameter = 1.75
filament_load_time = 0
filament_loading_speed = 28
filament_loading_speed_start = 3
filament_max_volumetric_speed = 0
filament_minimal_purge_on_wipe_tower = 15
filament_notes = ""
filament_ramming_parameters = "120 100 6.6 6.8 7.2 7.6 7.9 8.2 8.7 9.4 9.9 10.0| 0.05 6.6 0.45 6.8 0.95 7.8 1.45 8.3 1.95 9.7 2.45 10 2.95 7.6 3.45 7.6 3.95 7.6 4.45 7.6 4.95 7.6"
filament_retract_before_travel = nil
filament_retract_before_wipe = nil
filament_retract_layer_change = nil
filament_retract_length = nil
filament_retract_lift = nil
filament_retract_lift_above = nil
filament_retract_lift_below = nil
filament_retract_restart_extra = nil
filament_retract_speed = nil
filament_settings_id = "config.ini (CR-6 SE)"
filament_soluble = 0
filament_spool_weight = 0
filament_toolchange_delay = 0
filament_type = PLA
filament_unload_time = 0
filament_unloading_speed = 90
filament_unloading_speed_start = 100
filament_vendor = (Unknown)
filament_wipe = nil
fill_angle = 45
fill_density = 20%
fill_pattern = grid
first_layer_acceleration = 0
first_layer_bed_temperature = 60
first_layer_extrusion_width = 0.42
first_layer_height = 0.2
first_layer_speed = 20
first_layer_temperature = 205
full_fan_speed_layer = 0
gap_fill_speed = 30
gcode_comments = 0
gcode_flavor = marlin
gcode_label_objects = 0
high_current_on_filament_swap = 0
host_type = octoprint
infill_acceleration = 0
infill_anchor = 600%
infill_anchor_max = 50
infill_every_layers = 1
infill_extruder = 1
infill_extrusion_width = 0.45
infill_first = 0
infill_only_where_needed = 0
infill_overlap = 25%
infill_speed = 65
inherits_cummulative = ;;"Creality Ender-3 BLTouch"
interface_shells = 0
ironing = 0
ironing_flowrate = 15%
ironing_spacing = 0.1
ironing_speed = 15
ironing_type = top
layer_gcode = ;AFTER_LAYER_CHANGE\n;{layer_z}
layer_height = 0.2
machine_limits_usage = emit_to_gcode
machine_max_acceleration_e = 5000
machine_max_acceleration_extruding = 1250
machine_max_acceleration_retracting = 1250
machine_max_acceleration_x = 1000
machine_max_acceleration_y = 1000
machine_max_acceleration_z = 1000
machine_max_feedrate_e = 120
machine_max_feedrate_x = 200
machine_max_feedrate_y = 200
machine_max_feedrate_z = 12
machine_max_jerk_e = 1.5
machine_max_jerk_x = 8
machine_max_jerk_y = 8
machine_max_jerk_z = 0.4
machine_min_extruding_rate = 0
machine_min_travel_rate = 0
max_fan_speed = 100
max_layer_height = 0.28
max_print_height = 250
max_print_speed = 100
max_volumetric_speed = 0
min_fan_speed = 35
min_layer_height = 0.08
min_print_speed = 10
min_skirt_length = 4
notes =
nozzle_diameter = 0.4
only_retract_when_crossing_perimeters = 0
ooze_prevention = 0
output_filename_format = {input_filename_base}_{print_time}_{layer_height}mm_{temperature[0]}C_{filament_type[0]}_{printer_model}.gcode
overhangs = 0
parking_pos_retraction = 92
pause_print_gcode =
perimeter_acceleration = 0
perimeter_extruder = 1
perimeter_extrusion_width = 0.45
perimeter_speed = 40
perimeters = 2
physical_printer_settings_id =
post_process =
print_host =
print_settings_id = 0.20mm NORMAL @CREALITY
printer_model = ENDER3BLTOUCH
printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_CREALITY\nPRINTER_MODEL_ENDER3\nPRINTER_HAS_BOWDEN
printer_settings_id = Ender-3 BLTouch
printer_technology = FFF
printer_variant = 0.4
printer_vendor =
printhost_apikey =
printhost_cafile =
raft_layers = 0
remaining_times = 0
resolution = 0
retract_before_travel = 2
retract_before_wipe = 0%
retract_layer_change = 1
retract_length = 5
retract_length_toolchange = 1
retract_lift = 0
retract_lift_above = 0
retract_lift_below = 0
retract_restart_extra = 0
retract_restart_extra_toolchange = 0
retract_speed = 60
seam_position = nearest
silent_mode = 0
single_extruder_multi_material = 0
single_extruder_multi_material_priming = 0
skirt_distance = 3
skirt_height = 2
skirts = 1
slice_closing_radius = 0.049
slowdown_below_layer_time = 5
small_perimeter_speed = 25
solid_infill_below_area = 0
solid_infill_every_layers = 0
solid_infill_extruder = 1
solid_infill_extrusion_width = 0.45
solid_infill_speed = 50
spiral_vase = 0
standby_temperature_delta = -5
start_filament_gcode = "; Filament gcode\n"
start_gcode = G90 ; use absolute coordinates\nM83 ; extruder relative mode\nM140 S{first_layer_bed_temperature[0]} ; set final bed temp\nM104 S150 ; set temporary nozzle temp to prevent oozing during homing and auto bed leveling\nG4 S10 ; allow partial nozzle warmup\nG28 ; home all axis\n\nG1 Z50 F240\nG1 X2 Y10 F3000\nM104 S{first_layer_temperature[0]} ; set final nozzle temp\nM190 S{first_layer_bed_temperature[0]} ; wait for bed temp to stabilize\nM109 S{first_layer_temperature[0]} ; wait for nozzle temp to stabilize\nG1 Z0.28 F240\nG92 E0\nG1 Y140 E10 F1500 ; prime the nozzle\nG1 X2.3 F5000\nG92 E0\nG1 Y10 E10 F1200 ; prime the nozzle\nG92 E0
support_material = 1
support_material_angle = 0
support_material_auto = 1
support_material_buildplate_only = 0
support_material_contact_distance = 0.15
support_material_enforce_layers = 0
support_material_extruder = 0
support_material_extrusion_width = 0.45
support_material_interface_contact_loops = 0
support_material_interface_extruder = 0
support_material_interface_layers = 2
support_material_interface_spacing = 0.2
support_material_interface_speed = 100%
support_material_pattern = rectilinear
support_material_spacing = 2
support_material_speed = 70
support_material_synchronize_layers = 1
support_material_threshold = 50
support_material_with_sheath = 0
support_material_xy_spacing = 60%
temperature = 210
template_custom_gcode =
thin_walls = 0
threads = 12
thumbnails =
toolchange_gcode =
top_fill_pattern = monotonic
top_infill_extrusion_width = 0.4
top_solid_infill_speed = 45
top_solid_layers = 5
top_solid_min_thickness = 0
travel_speed = 150
use_firmware_retraction = 0
use_relative_e_distances = 1
use_volumetric_e = 0
variable_layer_height = 0
wipe = 0
wipe_into_infill = 0
wipe_into_objects = 0
wipe_tower = 0
wipe_tower_bridging = 10
wipe_tower_no_sparse_layers = 0
wipe_tower_rotation_angle = 0
wipe_tower_width = 60
wipe_tower_x = 170
wipe_tower_y = 140
wiping_volumes_extruders = 70,70
wiping_volumes_matrix = 0
xy_size_compensation = 0
z_offset = 0
+2
View File
@@ -0,0 +1,2 @@
upload_max_filesize = 200M
post_max_size = 250M
+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));
});