有构建板,支持多模型构建,但生成支撑的切片还有bug

This commit is contained in:
2026-04-11 01:50:30 +08:00
parent 975f06eb46
commit 3020957367
31 changed files with 3001 additions and 303 deletions

View File

@@ -1,8 +1,13 @@
import json
import trimesh
import uuid
import os
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 .models import db, User, PrintFile, SystemConfig
from .tasks import merge_and_slice_task
import os
import uuid
import configparser
@@ -11,21 +16,21 @@ from .tasks import slice_stl_task
main_bp = Blueprint('main', __name__)
def get_quality_presets():
preset_dir = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base')
presets = []
if os.path.exists(preset_dir):
for f in os.listdir(preset_dir):
if f.startswith('base_global_') and f.endswith('.inst.cfg'):
config = configparser.ConfigParser()
try:
config.read(os.path.join(preset_dir, f))
name = config.get('general', 'name', fallback=f)
presets.append((f, name))
except Exception as e:
pass
# Custom sort order or alphanumeric
return sorted(presets, key=lambda x: x[1])
# def get_quality_presets():
# preset_dir = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base')
# presets = []
# if os.path.exists(preset_dir):
# for f in os.listdir(preset_dir):
# if f.startswith('base_global_') and f.endswith('.inst.cfg'):
# config = configparser.ConfigParser()
# try:
# config.read(os.path.join(preset_dir, f))
# name = config.get('general', 'name', fallback=f)
# presets.append((f, name))
# except Exception as e:
# pass
# # Custom sort order or alphanumeric
# return sorted(presets, key=lambda x: x[1])
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -70,9 +75,9 @@ def set_language(lang):
response.set_cookie('lang', lang, max_age=60*60*24*365)
return response
@main_bp.route('/slice', methods=['GET', 'POST'])
@main_bp.route('/files', methods=['GET', 'POST'])
@login_required
def slice_page():
def files():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part', 'danger')
@@ -91,33 +96,51 @@ def slice_page():
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
file.save(filepath)
try:
mesh = trimesh.load(filepath)
# Check for overlapping faces or if the mesh is not watertight
# which can cause issues in CuraEngine
needs_repair = False
if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
needs_repair = True
if needs_repair:
# Attempt automatic repair
import trimesh.repair
trimesh.repair.fix_normals(mesh)
trimesh.repair.fix_inversion(mesh)
trimesh.repair.fix_winding(mesh)
trimesh.repair.fill_holes(mesh)
# Re-check after repair
if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'error': 'Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually before slicing.'}), 400
else:
flash('Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually.', 'danger')
os.remove(filepath)
return redirect(request.url)
else:
# Repair succeeded, rewrite file
mesh.export(filepath)
except Exception as e:
pass
print_file = PrintFile(
filename=unique_filename,
original_filename=original_filename,
file_type='stl',
user_id=current_user.id,
status='waiting'
status='uploaded' # Only display as uploaded, no automatic slicing
)
db.session.add(print_file)
db.session.commit()
# Start slicing task
quality_preset = request.form.get('quality', 'base_global_standard.inst.cfg')
slice_stl_task(print_file.id, filepath, quality_preset)
flash('File uploaded and slicing started!', 'success')
response = make_response(redirect(url_for('main.files')))
response.set_cookie('last_quality_preset', quality_preset, max_age=60*60*24*365)
return response
flash('File uploaded successfully!', 'success')
return redirect(url_for('main.files'))
presets = get_quality_presets()
last_quality = request.cookies.get('last_quality_preset', 'base_global_standard.inst.cfg')
return render_template('slice.html', presets=presets, last_quality=last_quality)
@main_bp.route('/files')
@login_required
def files():
# Order by newest first
user_files = PrintFile.query.filter_by(user_id=current_user.id).order_by(PrintFile.created_at.desc()).all()
user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
return render_template('files.html', files=user_files)
@main_bp.route('/api/files_status')
@@ -218,12 +241,176 @@ def require_admin():
flash('Admin access required', 'danger')
return redirect(url_for('main.index'))
@admin_bp.route('/settings')
@admin_bp.route('/settings', methods=['GET', 'POST'])
def settings():
configs = SystemConfig.query.all()
if request.method == 'POST':
# concurrent_slices = request.form.get('concurrent_slices')
offset_x = request.form.get('offset_x', '0')
offset_y = request.form.get('offset_y', '0')
# update or create config entries
for key, val in [('offset_x', offset_x), ('offset_y', offset_y)]:
conf = SystemConfig.query.filter_by(key=key).first()
if not conf:
conf = SystemConfig(key=key)
db.session.add(conf)
conf.value = val
db.session.commit()
flash('Settings updated successfully', 'success')
return redirect(url_for('admin.settings'))
configs = {c.key: c.value for c in SystemConfig.query.all()}
return render_template('admin_settings.html', configs=configs)
@admin_bp.route('/users')
def users():
all_users = User.query.order_by(User.created_at.desc()).all()
return render_template('admin_users.html', users=all_users)
def get_bed_dimensions():
try:
from flask import current_app
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:
from flask import current_app
path = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base')
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('_', ' ')
presets.append((f, name))
presets.sort(key=lambda x: x[1])
return presets
except:
return []
@main_bp.route('/plater')
@login_required
def plater():
w, h, hd = get_bed_dimensions()
presets = get_quality_presets()
# get offset configs
conf_x = SystemConfig.query.filter_by(key='offset_x').first()
conf_y = SystemConfig.query.filter_by(key='offset_y').first()
offset_x = float(conf_x.value) if conf_x else 0.0
offset_y = float(conf_y.value) if conf_y else 0.0
last_quality = request.cookies.get('last_quality_preset', '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('plater.html', w=w, h=h, hd=hd, presets=presets, last_quality=last_quality, models=models, offset_x=offset_x, offset_y=offset_y)
@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 not os.path.exists(proxy_path):
from stl_simplifier import simplify_stl
try:
simplify_stl(path, proxy_path, keep_ratio=0.1) # compress to 10%
except:
return send_file(path) # fallback to original if error
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():
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 += "等合并切片"
else:
combined_name += "合并切片"
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:
f.transform_matrix = json.dumps(p['raw_matrix'])
db.session.add(f)
db.session.commit()
if len(inputs) == 0:
return jsonify({'error': 'Invalid files'}), 400
if len(inputs) == 1:
# User is just generating gcode for a single original model, do NOT pollute list with new STL
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)
print_file = PrintFile(
filename=unique_filename,
original_filename=f"{combined_name}.stl",
file_type='stl',
user_id=current_user.id,
status='merging'
)
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!'})