import os import json import time import uuid import pandas as pd from flask import Flask, render_template, request, jsonify app = Flask(__name__) app.config['SECRET_KEY'] = 'smart-load-packer-secret' app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload # --- Error Handlers --- @app.errorhandler(500) def internal_error(error): return jsonify({'status': 'error', 'message': 'Internal Server Error', 'details': str(error)}), 500 @app.errorhandler(413) def request_entity_too_large(error): return jsonify({'status': 'error', 'message': 'File Too Large'}), 413 # --- 3D Packing Algorithm (Heuristic) --- class Item: def __init__(self, id, name, w, h, d, color, weight): self.id = id self.name = name self.w = float(w) self.h = float(h) self.d = float(d) self.color = color self.weight = float(weight) self.x = 0 self.y = 0 self.z = 0 self.volume = self.w * self.h * self.d def to_dict(self): return { 'id': self.id, 'name': self.name, 'w': self.w, 'h': self.h, 'd': self.d, 'x': self.x, 'y': self.y, 'z': self.z, 'color': self.color, 'weight': self.weight } class Bin: def __init__(self, w, h, d, max_weight): self.w = float(w) self.h = float(h) self.d = float(d) self.max_weight = float(max_weight) self.items = [] self.unpacked_items = [] # Potential placement points (x, y, z) # Start with origin self.points = [(0, 0, 0)] def intersect(self, i1, x2, y2, z2, w2, h2, d2): return ( i1.x < x2 + w2 and i1.x + i1.w > x2 and i1.y < y2 + h2 and i1.y + i1.h > y2 and i1.z < z2 + d2 and i1.z + i1.d > z2 ) def can_fit(self, item, x, y, z): # Check boundaries if x + item.w > self.w or y + item.h > self.h or z + item.d > self.d: return False # Check intersections for i in self.items: if self.intersect(i, x, y, z, item.w, item.h, item.d): return False return True def pack(self, items_to_pack): # Sort items by volume desc, then max dimension # Heuristic: Bigger items first items_to_pack.sort(key=lambda x: (x.volume, max(x.w, x.h, x.d)), reverse=True) for item in items_to_pack: placed = False # Sort points to try to pack bottom-up, back-left # Priority: Y (height/vertical) asc, Z (depth) asc, X (width) asc # Assuming Y is vertical axis self.points.sort(key=lambda p: (p[1], p[2], p[0])) for i, (px, py, pz) in enumerate(self.points): # Try standard orientation if self.can_fit(item, px, py, pz): item.x, item.y, item.z = px, py, pz self.items.append(item) self._add_new_points(item) placed = True break # Try rotation (swap W and D) - simple rotation on floor # Swap W and D if self.can_fit(item, px, py, pz): # Wait, I need to actually change dimensions to test rotation # But let's stick to simple non-rotation for MVP to avoid complexity pass if not placed: self.unpacked_items.append(item) else: # Remove the used point? Not necessarily, but for efficiency we could. # Actually, the used point is now inside an item, so it will fail can_fit for future items # But to keep list small, we can remove it. if (item.x, item.y, item.z) in self.points: self.points.remove((item.x, item.y, item.z)) def _add_new_points(self, item): # Add 3 new candidate points relative to the item # 1. Top of item p1 = (item.x, item.y + item.h, item.z) # 2. Right of item p2 = (item.x + item.w, item.y, item.z) # 3. Front of item p3 = (item.x, item.y, item.z + item.d) for p in [p1, p2, p3]: # Simple bound check optimization if p[0] < self.w and p[1] < self.h and p[2] < self.d: if p not in self.points: self.points.append(p) # --- Routes --- @app.route('/') def index(): return render_template('index.html') @app.route('/api/import-excel', methods=['POST']) def import_excel(): if 'file' not in request.files: return jsonify({'status': 'error', 'message': 'No file part'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'status': 'error', 'message': 'No selected file'}), 400 if file and (file.filename.endswith('.xlsx') or file.filename.endswith('.xls')): try: df = pd.read_excel(file) # Expected columns: name, width, height, depth, quantity, color, weight # Map columns vaguely items = [] for _, row in df.iterrows(): # Normalize keys row_keys = {k.lower(): v for k, v in row.to_dict().items()} # Helper to find key def get_val(keys_list, default=None): for k in keys_list: for rk in row_keys: if k in rk: return row_keys[rk] return default items.append({ 'name': str(get_val(['name', '名称', '名字'], 'Item')), 'width': float(get_val(['width', '宽', 'length', '长'], 10)), 'height': float(get_val(['height', '高'], 10)), 'depth': float(get_val(['depth', '深', 'width2'], 10)), 'quantity': int(get_val(['quantity', 'qty', '数量', 'count'], 1)), 'color': str(get_val(['color', '颜色'], '#888888')), 'weight': float(get_val(['weight', '重'], 1)) }) return jsonify({'status': 'success', 'items': items}) except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 return jsonify({'status': 'error', 'message': 'Invalid file type'}), 400 @app.route('/api/calculate', methods=['POST']) def calculate(): data = request.json container = data.get('container', {}) items_data = data.get('items', []) # Create Bin # Assuming Y is vertical height. bin_obj = Bin( w=container.get('width', 200), h=container.get('height', 200), d=container.get('depth', 400), max_weight=container.get('max_weight', 10000) ) # Create Items items = [] for i_data in items_data: qty = int(i_data.get('quantity', 1)) for _ in range(qty): items.append(Item( id=str(uuid.uuid4())[:8], name=i_data.get('name', 'Item'), w=i_data.get('width'), h=i_data.get('height'), d=i_data.get('depth'), color=i_data.get('color', '#888888'), weight=i_data.get('weight', 0) )) start_time = time.time() bin_obj.pack(items) end_time = time.time() # Calculate stats total_volume = bin_obj.w * bin_obj.h * bin_obj.d used_volume = sum(i.volume for i in bin_obj.items) volume_util = (used_volume / total_volume) * 100 if total_volume > 0 else 0 total_weight = sum(i.weight for i in bin_obj.items) return jsonify({ 'status': 'success', 'time_taken': f"{end_time - start_time:.4f}s", 'container': { 'w': bin_obj.w, 'h': bin_obj.h, 'd': bin_obj.d }, 'items_packed': [i.to_dict() for i in bin_obj.items], 'items_unpacked': [i.to_dict() for i in bin_obj.unpacked_items], 'stats': { 'volume_utilization': round(volume_util, 2), 'packed_count': len(bin_obj.items), 'unpacked_count': len(bin_obj.unpacked_items), 'total_weight': total_weight } }) if __name__ == '__main__': app.run(host='0.0.0.0', port=7860)