mirror of
https://github.com/ganglyglub2-gif/cotizador-impresion-3D.git
synced 2026-07-17 18:55:11 +00:00
Commit inicial del cotizador 3D
This commit is contained in:
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
upload_max_filesize = 200M
|
||||||
|
post_max_size = 250M
|
||||||
@@ -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
@@ -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); }
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user