有3d gcode预览,基本能按要求切片,但是缩放后切片会失败
This commit is contained in:
178
app/routes.py
178
app/routes.py
@@ -2,35 +2,21 @@ 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 .models import db, User, PrintFile, SystemConfig
|
||||
from .tasks import merge_and_slice_task
|
||||
import os
|
||||
import uuid
|
||||
import configparser
|
||||
from datetime import datetime
|
||||
from .tasks import slice_stl_task
|
||||
from .tasks import merge_and_slice_task, slice_stl_task
|
||||
from app import i18n_dict
|
||||
# import trimesh.repair
|
||||
from stl_simplifier import simplify_stl
|
||||
|
||||
|
||||
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])
|
||||
|
||||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
@@ -67,7 +53,7 @@ def index():
|
||||
|
||||
@main_bp.route('/set_language/<lang>')
|
||||
def set_language(lang):
|
||||
from app import i18n_dict
|
||||
|
||||
if lang not in i18n_dict:
|
||||
lang = 'en'
|
||||
# return to previous page
|
||||
@@ -96,35 +82,36 @@ def files():
|
||||
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
|
||||
# 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
|
||||
# pass
|
||||
|
||||
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)
|
||||
# if needs_repair:
|
||||
# # Attempt automatic 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
|
||||
# # 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,
|
||||
@@ -187,8 +174,12 @@ def preview_gcode(file_id):
|
||||
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.]"
|
||||
|
||||
return render_template('gcode_preview.html', file=print_file, content=content, line_count=line_count)
|
||||
|
||||
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('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
|
||||
@@ -200,9 +191,12 @@ def delete_file(file_id):
|
||||
stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename)
|
||||
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
||||
gcode_path = 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)
|
||||
|
||||
@@ -247,9 +241,21 @@ def settings():
|
||||
# concurrent_slices = request.form.get('concurrent_slices')
|
||||
offset_x = request.form.get('offset_x', '0')
|
||||
offset_y = request.form.get('offset_y', '0')
|
||||
default_infill = request.form.get('default_infill', '20')
|
||||
default_support = request.form.get('default_support', 'false')
|
||||
default_support_pattern = request.form.get('default_support_pattern', 'tree')
|
||||
default_quality = request.form.get('default_quality', 'base_global_standard.inst.cfg')
|
||||
|
||||
# update or create config entries
|
||||
for key, val in [('offset_x', offset_x), ('offset_y', offset_y)]:
|
||||
config_items = [
|
||||
('offset_x', offset_x),
|
||||
('offset_y', offset_y),
|
||||
('default_infill', default_infill),
|
||||
('default_support', default_support),
|
||||
('default_support_pattern', default_support_pattern),
|
||||
('default_quality', default_quality)
|
||||
]
|
||||
for key, val in config_items:
|
||||
conf = SystemConfig.query.filter_by(key=key).first()
|
||||
if not conf:
|
||||
conf = SystemConfig(key=key)
|
||||
@@ -260,17 +266,47 @@ def settings():
|
||||
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)
|
||||
presets = get_quality_presets()
|
||||
return render_template('admin_settings.html', configs=configs, presets=presets)
|
||||
|
||||
@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)
|
||||
|
||||
@admin_bp.route('/user/<int:user_id>/delete', methods=['POST'])
|
||||
def delete_user(user_id):
|
||||
user = User.query.get_or_404(user_id)
|
||||
if user.id == current_user.id:
|
||||
flash('You cannot delete yourself.', 'danger')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
print_files = PrintFile.query.filter_by(user_id=user.id).all()
|
||||
for print_file in print_files:
|
||||
stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename)
|
||||
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
||||
gcode_path = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
|
||||
proxy_path = stl_path + '.proxy.stl'
|
||||
|
||||
if os.path.exists(stl_path):
|
||||
try: os.remove(stl_path)
|
||||
except: pass
|
||||
if os.path.exists(proxy_path):
|
||||
try: os.remove(proxy_path)
|
||||
except: pass
|
||||
if os.path.exists(gcode_path):
|
||||
try: os.remove(gcode_path)
|
||||
except: pass
|
||||
|
||||
db.session.delete(print_file)
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f'User {user.username} and all their files have been deleted.', 'success')
|
||||
return redirect(url_for('admin.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)
|
||||
@@ -283,12 +319,13 @@ def get_bed_dimensions():
|
||||
|
||||
def get_quality_presets():
|
||||
try:
|
||||
from flask import current_app
|
||||
path = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base')
|
||||
|
||||
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', '').replace('base_', '').replace('_', ' ')
|
||||
name = f.replace('.inst.cfg', '')
|
||||
presets.append((f, name))
|
||||
presets.sort(key=lambda x: x[1])
|
||||
return presets
|
||||
@@ -301,16 +338,18 @@ 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
|
||||
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')
|
||||
|
||||
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)
|
||||
return render_template('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)
|
||||
|
||||
@main_bp.route('/file/<int:file_id>')
|
||||
@login_required
|
||||
@@ -330,9 +369,8 @@ def serve_proxy_file(file_id):
|
||||
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%
|
||||
simplify_stl(path, proxy_path, keep_ratio=0.05) # compress to 90%
|
||||
except:
|
||||
return send_file(path) # fallback to original if error
|
||||
if os.path.exists(proxy_path):
|
||||
@@ -363,8 +401,10 @@ def merge_and_slice():
|
||||
combined_name = ", ".join(names)
|
||||
if len(pieces) > 3:
|
||||
combined_name += "等合并切片"
|
||||
else:
|
||||
elif len(pieces) > 1:
|
||||
combined_name += "合并切片"
|
||||
else:
|
||||
combined_name += " 单独切片"
|
||||
|
||||
for p in pieces:
|
||||
f = PrintFile.query.get(p['file_id'])
|
||||
@@ -380,7 +420,9 @@ def merge_and_slice():
|
||||
if len(inputs) == 0:
|
||||
return jsonify({'error': 'Invalid files'}), 400
|
||||
|
||||
if len(inputs) == 1:
|
||||
is_edit = data.get('is_edit', False)
|
||||
|
||||
if len(inputs) == 1 and is_edit:
|
||||
# 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)
|
||||
|
||||
Reference in New Issue
Block a user