556 lines
23 KiB
Python
556 lines
23 KiB
Python
import json
|
|
import trimesh
|
|
import uuid
|
|
import os
|
|
import configparser
|
|
from datetime import datetime
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, session, make_response, send_file, abort, jsonify
|
|
from flask_login import login_user, logout_user, login_required, current_user
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from werkzeug.utils import secure_filename
|
|
from app.models import db, User, PrintFile, SystemConfig
|
|
from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task
|
|
from app import i18n_dict
|
|
# import trimesh.repair
|
|
from app.utils.stl_simplifier import simplify_stl
|
|
from app.routes.admin_routes import get_gcode_dir
|
|
|
|
|
|
main_bp = Blueprint('main', __name__)
|
|
|
|
|
|
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
|
|
|
|
|
def get_quota_info(user, file_type):
|
|
# Returns (quota_mb, current_size_bytes)
|
|
if user.is_admin:
|
|
quota_mb = 0.0
|
|
else:
|
|
conf = SystemConfig.query.filter_by(key=f"user_{user.id}_{file_type}_quota_mb").first()
|
|
quota_mb = float(conf.value) if conf else 0.0
|
|
|
|
if quota_mb == 0.0:
|
|
if user.is_guest:
|
|
def_conf = SystemConfig.query.filter_by(key=f"default_guest_{file_type}_quota_mb").first()
|
|
else:
|
|
def_conf = SystemConfig.query.filter_by(key=f"default_user_{file_type}_quota_mb").first()
|
|
quota_mb = float(def_conf.value) if def_conf else 0.0
|
|
|
|
user_files = PrintFile.query.filter_by(user_id=user.id).all()
|
|
current_size = 0
|
|
upload_dir = current_app.config.get('UPLOAD_FOLDER', 'uploads')
|
|
gcode_dir = get_gcode_dir()
|
|
|
|
for pf in user_files:
|
|
if file_type == 'stl' and not pf.original_filename.lower().endswith(('.gcode', '.gco', '.g')):
|
|
path = os.path.join(upload_dir, pf.filename)
|
|
if os.path.exists(path):
|
|
current_size += os.path.getsize(path)
|
|
elif file_type == 'gcode':
|
|
g_filename = pf.filename.rsplit('.', 1)[0] + '.gcode'
|
|
path = os.path.join(gcode_dir, g_filename)
|
|
if os.path.exists(path):
|
|
current_size += os.path.getsize(path)
|
|
else:
|
|
p2 = os.path.join(upload_dir, g_filename)
|
|
if os.path.exists(p2): current_size += os.path.getsize(p2)
|
|
|
|
return quota_mb, current_size
|
|
|
|
def check_quota(user, file_type, size_bytes):
|
|
if user.is_admin:
|
|
return True
|
|
quota_mb, current_size = get_quota_info(user, file_type)
|
|
if quota_mb <= 0.0:
|
|
return True
|
|
if current_size + size_bytes > quota_mb * 1024 * 1024:
|
|
return False
|
|
return True
|
|
|
|
# Guest User Middleware
|
|
@main_bp.before_app_request
|
|
def assign_guest_cookie():
|
|
if not current_user.is_authenticated:
|
|
guest_id = request.cookies.get('guest_id')
|
|
if not guest_id:
|
|
guest_id = str(uuid.uuid4())
|
|
user = User(username=f'guest_{guest_id[:8]}', is_guest=True, guest_cookie_id=guest_id)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
login_user(user)
|
|
# We will set the cookie in the response after request, see below
|
|
request.guest_id_to_set = guest_id
|
|
else:
|
|
user = User.query.filter_by(guest_cookie_id=guest_id).first()
|
|
if user:
|
|
login_user(user)
|
|
|
|
@main_bp.after_app_request
|
|
def set_guest_cookie(response):
|
|
if hasattr(request, 'guest_id_to_set'):
|
|
response.set_cookie('guest_id', request.guest_id_to_set, max_age=60*60*24*365) # 1 year
|
|
return response
|
|
|
|
# --- Main Routes ---
|
|
|
|
@main_bp.route('/')
|
|
@login_required
|
|
def index():
|
|
user_files = PrintFile.query.filter((PrintFile.user_id == current_user.id) | (current_user.is_admin)).all() if current_user.is_admin else PrintFile.query.filter_by(user_id=current_user.id).all()
|
|
|
|
stl_count = 0
|
|
stl_size = 0
|
|
gcode_count = 0
|
|
gcode_size = 0
|
|
|
|
upload_dir = current_app.config.get('UPLOAD_FOLDER', 'uploads')
|
|
gcode_dir = get_gcode_dir()
|
|
|
|
for f in user_files:
|
|
is_external_gcode = f.original_filename.lower().endswith(('.gcode', '.gco', '.g'))
|
|
|
|
if is_external_gcode:
|
|
gcode_count += 1
|
|
gcode_path = os.path.join(gcode_dir, f.filename.replace('.stl', '.gcode'))
|
|
if os.path.exists(gcode_path):
|
|
gcode_size += os.path.getsize(gcode_path)
|
|
else:
|
|
stl_count += 1
|
|
stl_path = os.path.join(upload_dir, f.filename)
|
|
if os.path.exists(stl_path):
|
|
stl_size += os.path.getsize(stl_path)
|
|
|
|
if f.status == 'sliced':
|
|
gcode_count += 1
|
|
gcode_filename = f.filename.rsplit('.', 1)[0] + '.gcode'
|
|
gcode_path = os.path.join(gcode_dir, gcode_filename)
|
|
if os.path.exists(gcode_path):
|
|
gcode_size += os.path.getsize(gcode_path)
|
|
else:
|
|
gcode_fallback = os.path.join(upload_dir, gcode_filename)
|
|
if os.path.exists(gcode_fallback):
|
|
gcode_size += os.path.getsize(gcode_fallback)
|
|
|
|
def format_size(size_bytes):
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes} B"
|
|
elif size_bytes < 1024 * 1024:
|
|
return f"{size_bytes / 1024:.2f} KB"
|
|
elif size_bytes < 1024 * 1024 * 1024:
|
|
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
|
else:
|
|
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
|
|
|
|
|
stl_quota_mb, stl_used_bytes = get_quota_info(current_user, 'stl')
|
|
gcode_quota_mb, gcode_used_bytes = get_quota_info(current_user, 'gcode')
|
|
|
|
return render_template('slice/index.html',
|
|
stl_count=stl_count,
|
|
stl_size_str=format_size(stl_size),
|
|
gcode_count=gcode_count,
|
|
gcode_size_str=format_size(gcode_size),
|
|
stl_used_bytes=stl_used_bytes,
|
|
stl_quota_mb=stl_quota_mb,
|
|
gcode_used_bytes=gcode_used_bytes,
|
|
gcode_quota_mb=gcode_quota_mb,
|
|
format_size=format_size
|
|
)
|
|
|
|
@main_bp.route('/set_language/<lang>')
|
|
def set_language(lang):
|
|
|
|
if lang not in i18n_dict:
|
|
lang = 'en'
|
|
# return to previous page
|
|
response = make_response(redirect(request.referrer or url_for('main.index')))
|
|
response.set_cookie('lang', lang, max_age=60*60*24*365)
|
|
return response
|
|
|
|
@main_bp.route('/files', methods=['GET', 'POST'])
|
|
@login_required
|
|
def files():
|
|
if request.method == 'POST':
|
|
if 'file' not in request.files:
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return jsonify({'error': 'No file part'}), 400
|
|
flash('No file part', 'danger')
|
|
return redirect(request.url)
|
|
|
|
uploaded_files = request.files.getlist('file')
|
|
success_count = 0
|
|
|
|
for file in uploaded_files:
|
|
if file.filename == '':
|
|
continue
|
|
if file and file.filename.lower().endswith('.stl'):
|
|
file.seek(0, os.SEEK_END)
|
|
size_bytes = file.tell()
|
|
file.seek(0, os.SEEK_SET)
|
|
if not check_quota(current_user, 'stl', size_bytes):
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return jsonify({'error': 'STL Storage Quota Exceeded'}), 400
|
|
flash('STL Storage Quota Exceeded', 'danger')
|
|
continue
|
|
|
|
original_filename = file.filename # Do not use secure_filename to keep Chinese characters
|
|
ext = os.path.splitext(original_filename)[1].lower()
|
|
if not ext:
|
|
ext = '.stl'
|
|
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
unique_filename = f"{timestamp}_{uuid.uuid4().hex}_{success_count}{ext}"
|
|
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
|
|
file.save(filepath)
|
|
|
|
print_file = PrintFile(
|
|
filename=unique_filename,
|
|
original_filename=original_filename,
|
|
file_type='stl',
|
|
user_id=current_user.id,
|
|
status='simplifying' # Set to simplifying while proxy is generated
|
|
)
|
|
db.session.add(print_file)
|
|
db.session.commit()
|
|
|
|
# Start background simplification
|
|
simplify_stl_task(print_file.id, filepath)
|
|
success_count += 1
|
|
|
|
if success_count > 0:
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return jsonify({'success': True, 'count': success_count})
|
|
flash(f'{success_count} file(s) uploaded successfully!', 'success')
|
|
else:
|
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
|
return jsonify({'error': 'No valid files uploaded'}), 400
|
|
flash('No valid files uploaded', 'danger')
|
|
|
|
return redirect(url_for('main.files'))
|
|
|
|
# Order by newest first
|
|
user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
|
|
return render_template('slice/files.html', files=user_files)
|
|
|
|
@main_bp.route('/api/files_status')
|
|
@login_required
|
|
def files_status():
|
|
files = PrintFile.query.filter_by(user_id=current_user.id).all()
|
|
return jsonify({str(f.id): f.status for f in files})
|
|
|
|
@main_bp.route('/download/<int:file_id>')
|
|
@login_required
|
|
def download_gcode(file_id):
|
|
print_file = PrintFile.query.get_or_404(file_id)
|
|
if print_file.user_id != current_user.id and not current_user.is_admin:
|
|
abort(403)
|
|
if print_file.status != 'sliced':
|
|
flash('File is not ready yet.', 'warning')
|
|
return redirect(url_for('main.files'))
|
|
|
|
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
|
conf = SystemConfig.query.filter_by(key='gcode_upload_folder').first()
|
|
gcode_dir = conf.value if (conf and conf.value and os.path.exists(conf.value)) else current_app.config['UPLOAD_FOLDER']
|
|
filepath = os.path.join(gcode_dir, gcode_filename)
|
|
if not os.path.exists(filepath):
|
|
fallback = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
|
|
if os.path.exists(fallback): filepath = fallback
|
|
|
|
if os.path.exists(filepath):
|
|
safe_name = print_file.original_filename.rsplit('.', 1)[0] + '.gcode'
|
|
return send_file(filepath, as_attachment=True, download_name=safe_name)
|
|
flash('GCode file not found. It might have been deleted.', 'danger')
|
|
return redirect(url_for('main.files'))
|
|
|
|
@main_bp.route('/preview_gcode/<int:file_id>')
|
|
@login_required
|
|
def preview_gcode(file_id):
|
|
print_file = PrintFile.query.get_or_404(file_id)
|
|
if print_file.user_id != current_user.id and not current_user.is_admin:
|
|
abort(403)
|
|
|
|
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
|
conf = SystemConfig.query.filter_by(key='gcode_upload_folder').first()
|
|
gcode_dir = conf.value if (conf and conf.value and os.path.exists(conf.value)) else current_app.config['UPLOAD_FOLDER']
|
|
filepath = os.path.join(gcode_dir, gcode_filename)
|
|
if not os.path.exists(filepath):
|
|
fallback = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
|
|
if os.path.exists(fallback): filepath = fallback
|
|
|
|
content = "File not found or not ready."
|
|
line_count = 0
|
|
if os.path.exists(filepath):
|
|
with open(filepath, 'r') as f:
|
|
lines = f.readlines()
|
|
line_count = len(lines)
|
|
content = "".join(lines[:500]) # Preview first 500 lines
|
|
if line_count > 500:
|
|
content += f"\n... \n[Preview truncated. Total lines: {line_count}. Please download to view full file.]"
|
|
|
|
w, h, hd = get_bed_dimensions()
|
|
configs = {c.key: c.value for c in SystemConfig.query.all()}
|
|
offset_x = float(configs.get('offset_x', '0.0'))
|
|
offset_y = float(configs.get('offset_y', '0.0'))
|
|
return render_template('slice/gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y)
|
|
|
|
@main_bp.route('/delete_file/<int:file_id>', methods=['POST'])
|
|
@login_required
|
|
def delete_file(file_id):
|
|
print_file = PrintFile.query.get_or_404(file_id)
|
|
if print_file.user_id != current_user.id and not current_user.is_admin:
|
|
abort(403)
|
|
|
|
stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename)
|
|
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
|
conf = SystemConfig.query.filter_by(key='gcode_upload_folder').first()
|
|
gcode_dir = conf.value if (conf and conf.value and os.path.exists(conf.value)) else current_app.config['UPLOAD_FOLDER']
|
|
gcode_path = os.path.join(gcode_dir, gcode_filename)
|
|
fallback_gcode = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
|
|
|
|
proxy_path = stl_path + '.proxy.stl'
|
|
|
|
if os.path.exists(stl_path):
|
|
os.remove(stl_path)
|
|
if os.path.exists(proxy_path):
|
|
os.remove(proxy_path)
|
|
if os.path.exists(gcode_path):
|
|
os.remove(gcode_path)
|
|
if os.path.exists(fallback_gcode):
|
|
os.remove(fallback_gcode)
|
|
|
|
db.session.delete(print_file)
|
|
db.session.commit()
|
|
flash(f"Deleted {print_file.original_filename} successfully.", 'success')
|
|
return redirect(url_for('main.files'))
|
|
|
|
# --- Auth Routes ---
|
|
|
|
def get_bed_dimensions():
|
|
try:
|
|
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
w = data['overrides']['machine_width']['default_value']
|
|
h = data['overrides']['machine_depth']['default_value']
|
|
hd = data['overrides']['machine_height']['default_value']
|
|
return w, h, hd
|
|
except:
|
|
return 200, 200, 200
|
|
|
|
def get_quality_presets():
|
|
try:
|
|
|
|
path = os.path.join(current_app.root_path, '..', 'print_config', 'quality', 'creality', 'presets')
|
|
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
|
|
presets = []
|
|
for f in files:
|
|
# name = f.replace('.inst.cfg', '').replace('base_', '').replace('_', ' ')
|
|
name = f.replace('.inst.cfg', '')
|
|
presets.append((f, name))
|
|
presets.sort(key=lambda x: x[1])
|
|
return presets
|
|
except:
|
|
return []
|
|
|
|
@main_bp.route('/plater')
|
|
@login_required
|
|
def plater():
|
|
quota_mb, current_size = get_quota_info(current_user, 'gcode')
|
|
quota_exceeded = (quota_mb > 0 and current_size >= quota_mb * 1024 * 1024)
|
|
|
|
w, h, hd = get_bed_dimensions()
|
|
presets = get_quality_presets()
|
|
|
|
configs = {c.key: c.value for c in SystemConfig.query.all()}
|
|
offset_x = float(configs.get('offset_x', '0.0'))
|
|
offset_y = float(configs.get('offset_y', '0.0'))
|
|
|
|
default_infill = configs.get('default_infill', '20')
|
|
default_support = configs.get('default_support', 'false')
|
|
default_support_pattern = configs.get('default_support_pattern', 'tree')
|
|
default_quality = configs.get('default_quality', 'base_global_standard.inst.cfg')
|
|
|
|
user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
|
|
models = [{'id': f.id, 'name': f.original_filename, 'status': f.status, 'url': url_for('main.serve_proxy_file', file_id=f.id), 'transform_matrix': f.transform_matrix} for f in user_files]
|
|
return render_template('slice/plater.html', w=w, h=h, hd=hd, presets=presets, last_quality=default_quality, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern, quota_exceeded=quota_exceeded)
|
|
|
|
@main_bp.route('/file/<int:file_id>')
|
|
@login_required
|
|
def serve_file(file_id):
|
|
f = PrintFile.query.get_or_404(file_id)
|
|
if f.user_id != current_user.id and not current_user.is_admin:
|
|
abort(403)
|
|
path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
|
|
return send_file(path)
|
|
|
|
@main_bp.route('/proxy/<int:file_id>')
|
|
@login_required
|
|
def serve_proxy_file(file_id):
|
|
f = PrintFile.query.get_or_404(file_id)
|
|
if f.user_id != current_user.id and not current_user.is_admin:
|
|
abort(403)
|
|
path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
|
|
proxy_path = path + '.proxy.stl'
|
|
if os.path.exists(proxy_path):
|
|
return send_file(proxy_path)
|
|
return send_file(path)
|
|
|
|
@main_bp.route('/api/merge_and_slice', methods=['POST'])
|
|
@login_required
|
|
def merge_and_slice():
|
|
quota_mb, current_size = get_quota_info(current_user, 'gcode')
|
|
if quota_mb > 0 and current_size >= quota_mb * 1024 * 1024:
|
|
return jsonify({'success': False, 'error': 'GCode Storage Quota Exceeded. Please delete some files first.'})
|
|
data = request.json
|
|
pieces = data.get('pieces', [])
|
|
quality = data.get('quality', 'base_global_standard.inst.cfg')
|
|
infill_density = data.get('infill', '20')
|
|
support_enable = data.get('support', 'false')
|
|
support_pattern = data.get('support_pattern', 'lines')
|
|
|
|
if not pieces:
|
|
return jsonify({'error': 'No pieces provided'}), 400
|
|
|
|
inputs = []
|
|
# Build a combined name
|
|
names = []
|
|
for p in pieces[:3]: # Cap names at 3 to avoid super long string
|
|
f = PrintFile.query.get(p['file_id'])
|
|
if f and (f.user_id == current_user.id or current_user.is_admin):
|
|
names.append(f.original_filename.replace('.stl', ''))
|
|
|
|
combined_name = ", ".join(names)
|
|
if len(pieces) > 3:
|
|
combined_name += "等合并切片"
|
|
elif len(pieces) > 1:
|
|
combined_name += "合并切片"
|
|
else:
|
|
combined_name += " 单独切片"
|
|
|
|
is_edit = data.get('is_edit', False)
|
|
|
|
for p in pieces:
|
|
f = PrintFile.query.get(p['file_id'])
|
|
if f and (f.user_id == current_user.id or current_user.is_admin):
|
|
path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
|
|
inputs.append((path, p['matrix']))
|
|
# 只有在单一编辑模式才修改原模型的矩阵 (如果多模型/新建模式,我们不修改原模型,而是后续记录到新的包含实体上)
|
|
if 'raw_matrix' in p and is_edit and len(pieces) == 1:
|
|
f.transform_matrix = json.dumps({
|
|
"is_composite": False,
|
|
"matrix": p['raw_matrix'],
|
|
"settings": {
|
|
"quality": quality,
|
|
"infill": infill_density,
|
|
"support": support_enable,
|
|
"support_pattern": support_pattern
|
|
}
|
|
})
|
|
db.session.add(f)
|
|
|
|
db.session.commit()
|
|
|
|
target_file_id = data.get('target_file_id')
|
|
|
|
if is_edit and target_file_id:
|
|
print_file = PrintFile.query.get(target_file_id)
|
|
if not print_file:
|
|
return jsonify({'error': 'Original file not found'}), 404
|
|
print_file.status = 'merging'
|
|
|
|
if print_file.transform_matrix and 'is_composite' in print_file.transform_matrix:
|
|
composite_data = {
|
|
"is_composite": True,
|
|
"parts": [],
|
|
"settings": {
|
|
"quality": quality,
|
|
"infill": infill_density,
|
|
"support": support_enable,
|
|
"support_pattern": support_pattern
|
|
}
|
|
}
|
|
for p in pieces:
|
|
pf = PrintFile.query.get(p['file_id'])
|
|
if pf:
|
|
composite_data['parts'].append({
|
|
"file_id": pf.id,
|
|
"name": pf.original_filename,
|
|
"url": url_for('main.serve_proxy_file', file_id=pf.id),
|
|
"raw_matrix": p.get('raw_matrix', p['matrix'])
|
|
})
|
|
print_file.transform_matrix = json.dumps(composite_data)
|
|
elif len(pieces) == 1:
|
|
print_file.transform_matrix = json.dumps({
|
|
"is_composite": False,
|
|
"matrix": pieces[0].get('raw_matrix', pieces[0]['matrix']),
|
|
"settings": {
|
|
"quality": quality,
|
|
"infill": infill_density,
|
|
"support": support_enable,
|
|
"support_pattern": support_pattern
|
|
}
|
|
})
|
|
|
|
db.session.commit()
|
|
|
|
temp_filename = f"temp_edit_{uuid.uuid4().hex}.stl"
|
|
temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename)
|
|
|
|
merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=True)
|
|
elif len(inputs) == 1 and is_edit:
|
|
target_file_id = pieces[0]['file_id']
|
|
print_file = PrintFile.query.get(target_file_id)
|
|
if not print_file:
|
|
return jsonify({'error': 'Original file not found'}), 404
|
|
print_file.status = 'merging'
|
|
db.session.commit()
|
|
|
|
# We still need to apply transforms to a temporary STL to generate correct GCode
|
|
temp_filename = f"temp_{uuid.uuid4().hex}.stl"
|
|
temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename)
|
|
|
|
merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=True)
|
|
else:
|
|
# Multiple models, create a new "Merged Slice" PrintFile entry to keep track of combination
|
|
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
|
unique_filename = f"{timestamp}_{uuid.uuid4().hex}.stl"
|
|
merged_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
|
|
|
|
# 构建组合文件元数据树 (is_composite: true)
|
|
composite_data = {
|
|
"is_composite": True,
|
|
"parts": [],
|
|
"settings": {
|
|
"quality": quality,
|
|
"infill": infill_density,
|
|
"support": support_enable,
|
|
"support_pattern": support_pattern
|
|
}
|
|
}
|
|
for p in pieces:
|
|
pf = PrintFile.query.get(p['file_id'])
|
|
if pf:
|
|
composite_data['parts'].append({
|
|
"file_id": pf.id,
|
|
"name": pf.original_filename,
|
|
"url": url_for('main.serve_proxy_file', file_id=pf.id),
|
|
"raw_matrix": p.get('raw_matrix', p['matrix'])
|
|
})
|
|
|
|
print_file = PrintFile(
|
|
filename=unique_filename,
|
|
original_filename=f"{combined_name}.stl",
|
|
file_type='stl',
|
|
user_id=current_user.id,
|
|
status='merging',
|
|
transform_matrix=json.dumps(composite_data)
|
|
)
|
|
db.session.add(print_file)
|
|
db.session.commit()
|
|
|
|
merge_and_slice_task(print_file.id, inputs, merged_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=False)
|
|
|
|
return jsonify({'success': True, 'message': 'Plater slice queued!'})
|
|
|