Spaces:
Paused
Paused
| 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 --- | |
| def internal_error(error): | |
| return jsonify({'status': 'error', 'message': 'Internal Server Error', 'details': str(error)}), 500 | |
| 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 --- | |
| def index(): | |
| return render_template('index.html') | |
| 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 | |
| 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) | |