3v324v23's picture
Enhance functionality: Excel import, bug fixes, UI improvements
8a67525
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)