From 39ad677f6b1e1af379625a852fc9c9d5a7e3c840 Mon Sep 17 00:00:00 2001 From: ganglyglub2-gif Date: Fri, 7 Nov 2025 11:20:45 -0600 Subject: [PATCH] Commit inicial del cotizador 3D --- .gitignore | 6 + Dockerfile | 43 ++++++ docker-compose.yml | 12 ++ profiles/Ender3_Bltouch.ini | 255 ++++++++++++++++++++++++++++++++ uploads.ini | 2 + www/calculo_precio.php | 123 ++++++++++++++++ www/estilos.css | 275 +++++++++++++++++++++++++++++++++++ www/impresoras.json | 43 ++++++ www/index.php | 283 ++++++++++++++++++++++++++++++++++++ www/precios.json | 19 +++ www/procesar.php | 151 +++++++++++++++++++ www/runPrusa.js | 44 ++++++ 12 files changed, 1256 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 profiles/Ender3_Bltouch.ini create mode 100644 uploads.ini create mode 100644 www/calculo_precio.php create mode 100644 www/estilos.css create mode 100644 www/impresoras.json create mode 100644 www/index.php create mode 100644 www/precios.json create mode 100644 www/procesar.php create mode 100644 www/runPrusa.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76932fe --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ae99540 --- /dev/null +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dcae829 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/profiles/Ender3_Bltouch.ini b/profiles/Ender3_Bltouch.ini new file mode 100644 index 0000000..d9de345 --- /dev/null +++ b/profiles/Ender3_Bltouch.ini @@ -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 diff --git a/uploads.ini b/uploads.ini new file mode 100644 index 0000000..23cdddd --- /dev/null +++ b/uploads.ini @@ -0,0 +1,2 @@ +upload_max_filesize = 200M +post_max_size = 250M diff --git a/www/calculo_precio.php b/www/calculo_precio.php new file mode 100644 index 0000000..dc9644f --- /dev/null +++ b/www/calculo_precio.php @@ -0,0 +1,123 @@ + [ + '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 + ]; +} +?> \ No newline at end of file diff --git a/www/estilos.css b/www/estilos.css new file mode 100644 index 0000000..089ac7d --- /dev/null +++ b/www/estilos.css @@ -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); } +} \ No newline at end of file diff --git a/www/impresoras.json b/www/impresoras.json new file mode 100644 index 0000000..0830202 --- /dev/null +++ b/www/impresoras.json @@ -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" + } + } + +} \ No newline at end of file diff --git a/www/index.php b/www/index.php new file mode 100644 index 0000000..1a33867 --- /dev/null +++ b/www/index.php @@ -0,0 +1,283 @@ + + + + + + Cotizador 3D + + + + + + + + + +
+

Cotizador de Impresión 3D

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+
+

Procesando archivo...

+
+
+
+
+
+ + \ No newline at end of file diff --git a/www/precios.json b/www/precios.json new file mode 100644 index 0000000..ce0b397 --- /dev/null +++ b/www/precios.json @@ -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 + } +} \ No newline at end of file diff --git a/www/procesar.php b/www/procesar.php new file mode 100644 index 0000000..46dfc3f --- /dev/null +++ b/www/procesar.php @@ -0,0 +1,151 @@ +❌ Error al mover el archivo.'; + 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 '
⚠️ Error al generar G-code:
' . implode("
", $output) . '
'; + exit; + } + + $result = json_decode($output[0], true); + if (!$result) { + echo '
⚠️ No se pudo leer la información del G-code.
'; + 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 ' +
+
+

📊 Datos Base

+
Filamento usado: ' . number_format($filamento_mm, 2) . ' mm
+
Volumen usado: ' . number_format($filamento_cm3, 2) . ' cm³
+
Tiempo estimado: ' . htmlspecialchars($tiempo_estimado) . '
+ +
+

🛡️ Con Margen de Seguridad (10%)

+
Filamento usado (seguridad): ' . number_format($filamento_mm_seg, 2) . ' mm
+
Volumen usado (seguridad): ' . number_format($filamento_cm3_seg, 2) . ' cm³
+
Tiempo estimado (seguridad): ' . htmlspecialchars($tiempo_seg) . '
+
+ +
+

💰 Cotización Estimada

+
Impresora: ' . htmlspecialchars(ucfirst($impresoraSeleccionada)) . '
+
Material: ' . htmlspecialchars(ucfirst($materialSeleccionado)) . '
+
+
• Costo Material: ' . number_format($cotizacion['costo_material'], 2) . ' MXN
+
• Costo Tiempo: ' . number_format($cotizacion['costo_tiempo'], 2) . ' MXN
+
• Subtotal Producción: ' . number_format($cotizacion['subtotal_produccion'], 2) . ' MXN
+
• Consumible Impresión: ' . number_format($cotizacion['consumible_impresion'], 2) . ' MXN
+
• Costo Operación: ' . number_format($cotizacion['costo_operacion'], 2) . ' MXN
+
• Subtotal Operación: ' . number_format($cotizacion['subtotal_operacion'], 2) . ' MXN
+
• Costo Final: ' . number_format($cotizacion['costo_final_impresion'], 2) . ' MXN
+
• Indirectos: ' . number_format($cotizacion['indirectos'], 2) . ' MXN
+
+
💵 Precio Final Estimado: ' . number_format($cotizacion['precio_final'], 2) . ' MXN
+ +
+
'; + + // === VISUALIZADOR 3D === + echo ' +
+ +
+

Controles del Visor 3D

+ +
+'; + + + $stlUrl = 'uploads/' . htmlspecialchars(basename($stlPath)); + echo ''; + + + + + } else { + echo '
❌ No se recibió un archivo STL válido.
'; + } +} else { + echo '
⚠️ Método no permitido.
'; +} + +// --- 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; +} +?> \ No newline at end of file diff --git a/www/runPrusa.js b/www/runPrusa.js new file mode 100644 index 0000000..75ffaf0 --- /dev/null +++ b/www/runPrusa.js @@ -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)); +}); \ No newline at end of file