有3d gcode预览,基本能按要求切片,但是缩放后切片会失败
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,3 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
uploads/*
|
uploads/*
|
||||||
venv/*
|
tmp/*
|
||||||
instance/*
|
|
||||||
huey_queue.db
|
|
||||||
163
app/conf_parse.py
Normal file
163
app/conf_parse.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import configparser
|
||||||
|
import json
|
||||||
|
import copy
|
||||||
|
import math
|
||||||
|
|
||||||
|
class ConfParse:
|
||||||
|
def __init__(self, config_def_json_files_paths):
|
||||||
|
self.config_def_json_files_paths = config_def_json_files_paths
|
||||||
|
self.configs = {}
|
||||||
|
for file in self.config_def_json_files_paths:
|
||||||
|
with open(file, 'r') as f:
|
||||||
|
jf = json.load(f)
|
||||||
|
if "settings" in jf:
|
||||||
|
setting_json = jf["settings"]
|
||||||
|
for key, value in setting_json.items():
|
||||||
|
# print(key, value)
|
||||||
|
all_items = self._expend_children(value["children"])
|
||||||
|
for item_key, item_value in all_items.items():
|
||||||
|
self.configs[item_key] = item_value
|
||||||
|
elif "overrides" in jf:
|
||||||
|
override_json = jf["overrides"]
|
||||||
|
for key, value in override_json.items():
|
||||||
|
if key in self.configs:
|
||||||
|
for item_key, item_value in value.items():
|
||||||
|
self.configs[key][item_key] = item_value
|
||||||
|
else:
|
||||||
|
self.configs[key] = value
|
||||||
|
|
||||||
|
def _expend_children(self, config):
|
||||||
|
tmp_c = {}
|
||||||
|
for key,val in config.items():
|
||||||
|
if "children" in val:
|
||||||
|
tmp_c.update(self._expend_children(val["children"]))
|
||||||
|
val.pop("children", None)
|
||||||
|
tmp_c[key] = val
|
||||||
|
return tmp_c
|
||||||
|
|
||||||
|
def add_inst_cfg(self, inst_cfg_files_paths):
|
||||||
|
copy_settings = copy.deepcopy(self.configs)
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
for file in inst_cfg_files_paths:
|
||||||
|
config.read(file)
|
||||||
|
if config.has_section('values'):
|
||||||
|
for key, val in config.items('values'):
|
||||||
|
v = str(val)
|
||||||
|
if key not in copy_settings:
|
||||||
|
copy_settings[key] = {}
|
||||||
|
if v.startswith("="):
|
||||||
|
copy_settings[key]["value"] = v[1:]
|
||||||
|
else:
|
||||||
|
copy_settings[key]["value"] = v
|
||||||
|
return copy_settings
|
||||||
|
|
||||||
|
|
||||||
|
def parse_configs(self, settings):
|
||||||
|
class ConfigStr(str):
|
||||||
|
def __mul__(self, other): raise TypeError()
|
||||||
|
def __rmul__(self, other): raise TypeError()
|
||||||
|
def __add__(self, other): raise TypeError()
|
||||||
|
def __radd__(self, other): raise TypeError()
|
||||||
|
def __sub__(self, other): raise TypeError()
|
||||||
|
def __rsub__(self, other): raise TypeError()
|
||||||
|
def __truediv__(self, other): raise TypeError()
|
||||||
|
def __rtruediv__(self, other): raise TypeError()
|
||||||
|
def __pow__(self, other): raise TypeError()
|
||||||
|
def __rpow__(self, other): raise TypeError()
|
||||||
|
|
||||||
|
parsed_settings = copy.deepcopy(settings)
|
||||||
|
|
||||||
|
last_unparsed = -1
|
||||||
|
while True:
|
||||||
|
unparsed = 0
|
||||||
|
|
||||||
|
# 构建上下文环境变量上下文,用作eval的变量替换
|
||||||
|
context = {}
|
||||||
|
for k, v in parsed_settings.items():
|
||||||
|
# 上下文里的变量取值:优先使用 value(如果有),否则使用 default_value
|
||||||
|
if "value" in v:
|
||||||
|
val = v["value"]
|
||||||
|
elif "default_value" in v:
|
||||||
|
val = v["default_value"]
|
||||||
|
else:
|
||||||
|
val = None
|
||||||
|
|
||||||
|
if isinstance(val, str):
|
||||||
|
if val.lower() == "true":
|
||||||
|
val = True
|
||||||
|
elif val.lower() == "false":
|
||||||
|
val = False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
val = int(val)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
val = float(val)
|
||||||
|
except ValueError:
|
||||||
|
val = ConfigStr(val)
|
||||||
|
context[k] = val
|
||||||
|
|
||||||
|
# 自定义函数实现
|
||||||
|
def resolveOrValue(key):
|
||||||
|
return context.get(key)
|
||||||
|
|
||||||
|
def extruderValues(key):
|
||||||
|
# 兼容简易的多挤出机查询逻辑,目前单挤出机环境下返回一个单元素列表
|
||||||
|
return [context.get(key)]
|
||||||
|
|
||||||
|
def extruderValue(extruder_position, key):
|
||||||
|
# 对于单挤出机环境或者全局配置,直接忽略 extruder_position,返回指定的 key 对应的值
|
||||||
|
return context.get(key)
|
||||||
|
|
||||||
|
def defaultExtruderPosition():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 提供基础的运算函数支持
|
||||||
|
builtin_funcs = {
|
||||||
|
"max": max,
|
||||||
|
"min": min,
|
||||||
|
"abs": abs,
|
||||||
|
"round": round,
|
||||||
|
"int": int,
|
||||||
|
"float": float,
|
||||||
|
"bool": bool,
|
||||||
|
"math": math,
|
||||||
|
"resolveOrValue": resolveOrValue,
|
||||||
|
"extruderValues": extruderValues,
|
||||||
|
"extruderValue": extruderValue,
|
||||||
|
"defaultExtruderPosition": defaultExtruderPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val_dict in parsed_settings.items():
|
||||||
|
for field, field_val in val_dict.items():
|
||||||
|
# 仅对字符串进行尝试计算
|
||||||
|
if "type" == field:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(field_val, str):
|
||||||
|
try:
|
||||||
|
# 如果是一个普通的纯字符串,比如分类名(如"Machine Type" ),eval可能会抛出SyntaxError或NameError
|
||||||
|
# 如果它是一个python表达式,则会被顺利计算出结果
|
||||||
|
evaluated = eval(field_val, {"__builtins__": builtin_funcs}, context)
|
||||||
|
# 避免将原本只是用来作类型声明的 "int"/"float" 纯字符串,被错误求值为内置 Python type class 打断 JSON 序列化
|
||||||
|
if evaluated != field_val and not isinstance(evaluated, type):
|
||||||
|
if val_dict.get("type") == "str" and not isinstance(evaluated, str):
|
||||||
|
if isinstance(evaluated, (list, dict)):
|
||||||
|
import json
|
||||||
|
val_dict[field] = json.dumps(evaluated).replace(" ", "")
|
||||||
|
else:
|
||||||
|
val_dict[field] = str(evaluated)
|
||||||
|
else:
|
||||||
|
val_dict[field] = evaluated
|
||||||
|
except Exception:
|
||||||
|
# 解析失败(例如是个普通英文字符串或者依赖还没被解开)则暂不做处理
|
||||||
|
unparsed += 1
|
||||||
|
|
||||||
|
# 如果在这一轮中未解析出的表达式数量不再减少,说明已经到达极限,跳出循环
|
||||||
|
if unparsed == last_unparsed:
|
||||||
|
break
|
||||||
|
last_unparsed = unparsed
|
||||||
|
|
||||||
|
return parsed_settings
|
||||||
|
|
||||||
|
|
||||||
176
app/routes.py
176
app/routes.py
@@ -2,35 +2,21 @@ import json
|
|||||||
import trimesh
|
import trimesh
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
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 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 flask_login import login_user, logout_user, login_required, current_user
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from .models import db, User, PrintFile, SystemConfig
|
from .models import db, User, PrintFile, SystemConfig
|
||||||
from .tasks import merge_and_slice_task
|
from .tasks import merge_and_slice_task, slice_stl_task
|
||||||
import os
|
from app import i18n_dict
|
||||||
import uuid
|
# import trimesh.repair
|
||||||
import configparser
|
from stl_simplifier import simplify_stl
|
||||||
from datetime import datetime
|
|
||||||
from .tasks import slice_stl_task
|
|
||||||
|
|
||||||
main_bp = Blueprint('main', __name__)
|
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')
|
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
@@ -67,7 +53,7 @@ def index():
|
|||||||
|
|
||||||
@main_bp.route('/set_language/<lang>')
|
@main_bp.route('/set_language/<lang>')
|
||||||
def set_language(lang):
|
def set_language(lang):
|
||||||
from app import i18n_dict
|
|
||||||
if lang not in i18n_dict:
|
if lang not in i18n_dict:
|
||||||
lang = 'en'
|
lang = 'en'
|
||||||
# return to previous page
|
# return to previous page
|
||||||
@@ -96,35 +82,36 @@ def files():
|
|||||||
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
|
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
|
||||||
file.save(filepath)
|
file.save(filepath)
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
mesh = trimesh.load(filepath)
|
# mesh = trimesh.load(filepath)
|
||||||
# Check for overlapping faces or if the mesh is not watertight
|
# # Check for overlapping faces or if the mesh is not watertight
|
||||||
# which can cause issues in CuraEngine
|
# # which can cause issues in CuraEngine
|
||||||
needs_repair = False
|
# needs_repair = False
|
||||||
if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
|
# if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
|
||||||
needs_repair = True
|
# # needs_repair = True
|
||||||
|
# pass
|
||||||
|
|
||||||
if needs_repair:
|
# if needs_repair:
|
||||||
# Attempt automatic 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
|
# trimesh.repair.fix_normals(mesh)
|
||||||
if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
|
# trimesh.repair.fix_inversion(mesh)
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
# trimesh.repair.fix_winding(mesh)
|
||||||
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
|
# trimesh.repair.fill_holes(mesh)
|
||||||
else:
|
|
||||||
flash('Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually.', 'danger')
|
# # Re-check after repair
|
||||||
os.remove(filepath)
|
# if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
|
||||||
return redirect(request.url)
|
# if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
else:
|
# 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
|
||||||
# Repair succeeded, rewrite file
|
# else:
|
||||||
mesh.export(filepath)
|
# flash('Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually.', 'danger')
|
||||||
except Exception as e:
|
# os.remove(filepath)
|
||||||
pass
|
# return redirect(request.url)
|
||||||
|
# else:
|
||||||
|
# # Repair succeeded, rewrite file
|
||||||
|
# mesh.export(filepath)
|
||||||
|
# except Exception as e:
|
||||||
|
# pass
|
||||||
|
|
||||||
print_file = PrintFile(
|
print_file = PrintFile(
|
||||||
filename=unique_filename,
|
filename=unique_filename,
|
||||||
@@ -188,7 +175,11 @@ def preview_gcode(file_id):
|
|||||||
if line_count > 500:
|
if line_count > 500:
|
||||||
content += f"\n... \n[Preview truncated. Total lines: {line_count}. Please download to view full file.]"
|
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'])
|
@main_bp.route('/delete_file/<int:file_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -200,9 +191,12 @@ def delete_file(file_id):
|
|||||||
stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename)
|
stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename)
|
||||||
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode'
|
||||||
gcode_path = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
|
gcode_path = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename)
|
||||||
|
proxy_path = stl_path + '.proxy.stl'
|
||||||
|
|
||||||
if os.path.exists(stl_path):
|
if os.path.exists(stl_path):
|
||||||
os.remove(stl_path)
|
os.remove(stl_path)
|
||||||
|
if os.path.exists(proxy_path):
|
||||||
|
os.remove(proxy_path)
|
||||||
if os.path.exists(gcode_path):
|
if os.path.exists(gcode_path):
|
||||||
os.remove(gcode_path)
|
os.remove(gcode_path)
|
||||||
|
|
||||||
@@ -247,9 +241,21 @@ def settings():
|
|||||||
# concurrent_slices = request.form.get('concurrent_slices')
|
# concurrent_slices = request.form.get('concurrent_slices')
|
||||||
offset_x = request.form.get('offset_x', '0')
|
offset_x = request.form.get('offset_x', '0')
|
||||||
offset_y = request.form.get('offset_y', '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
|
# 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()
|
conf = SystemConfig.query.filter_by(key=key).first()
|
||||||
if not conf:
|
if not conf:
|
||||||
conf = SystemConfig(key=key)
|
conf = SystemConfig(key=key)
|
||||||
@@ -260,17 +266,47 @@ def settings():
|
|||||||
return redirect(url_for('admin.settings'))
|
return redirect(url_for('admin.settings'))
|
||||||
|
|
||||||
configs = {c.key: c.value for c in SystemConfig.query.all()}
|
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')
|
@admin_bp.route('/users')
|
||||||
def users():
|
def users():
|
||||||
all_users = User.query.order_by(User.created_at.desc()).all()
|
all_users = User.query.order_by(User.created_at.desc()).all()
|
||||||
return render_template('admin_users.html', users=all_users)
|
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():
|
def get_bed_dimensions():
|
||||||
try:
|
try:
|
||||||
from flask import current_app
|
|
||||||
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
|
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
@@ -283,12 +319,13 @@ def get_bed_dimensions():
|
|||||||
|
|
||||||
def get_quality_presets():
|
def get_quality_presets():
|
||||||
try:
|
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')]
|
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
|
||||||
presets = []
|
presets = []
|
||||||
for f in files:
|
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.append((f, name))
|
||||||
presets.sort(key=lambda x: x[1])
|
presets.sort(key=lambda x: x[1])
|
||||||
return presets
|
return presets
|
||||||
@@ -301,16 +338,18 @@ def plater():
|
|||||||
w, h, hd = get_bed_dimensions()
|
w, h, hd = get_bed_dimensions()
|
||||||
presets = get_quality_presets()
|
presets = get_quality_presets()
|
||||||
|
|
||||||
# get offset configs
|
configs = {c.key: c.value for c in SystemConfig.query.all()}
|
||||||
conf_x = SystemConfig.query.filter_by(key='offset_x').first()
|
offset_x = float(configs.get('offset_x', '0.0'))
|
||||||
conf_y = SystemConfig.query.filter_by(key='offset_y').first()
|
offset_y = float(configs.get('offset_y', '0.0'))
|
||||||
offset_x = float(conf_x.value) if conf_x else 0.0
|
|
||||||
offset_y = float(conf_y.value) if conf_y else 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()
|
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]
|
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>')
|
@main_bp.route('/file/<int:file_id>')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -330,9 +369,8 @@ def serve_proxy_file(file_id):
|
|||||||
path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
|
path = os.path.join(current_app.config['UPLOAD_FOLDER'], f.filename)
|
||||||
proxy_path = path + '.proxy.stl'
|
proxy_path = path + '.proxy.stl'
|
||||||
if not os.path.exists(proxy_path):
|
if not os.path.exists(proxy_path):
|
||||||
from stl_simplifier import simplify_stl
|
|
||||||
try:
|
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:
|
except:
|
||||||
return send_file(path) # fallback to original if error
|
return send_file(path) # fallback to original if error
|
||||||
if os.path.exists(proxy_path):
|
if os.path.exists(proxy_path):
|
||||||
@@ -363,8 +401,10 @@ def merge_and_slice():
|
|||||||
combined_name = ", ".join(names)
|
combined_name = ", ".join(names)
|
||||||
if len(pieces) > 3:
|
if len(pieces) > 3:
|
||||||
combined_name += "等合并切片"
|
combined_name += "等合并切片"
|
||||||
else:
|
elif len(pieces) > 1:
|
||||||
combined_name += "合并切片"
|
combined_name += "合并切片"
|
||||||
|
else:
|
||||||
|
combined_name += " 单独切片"
|
||||||
|
|
||||||
for p in pieces:
|
for p in pieces:
|
||||||
f = PrintFile.query.get(p['file_id'])
|
f = PrintFile.query.get(p['file_id'])
|
||||||
@@ -380,7 +420,9 @@ def merge_and_slice():
|
|||||||
if len(inputs) == 0:
|
if len(inputs) == 0:
|
||||||
return jsonify({'error': 'Invalid files'}), 400
|
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
|
# User is just generating gcode for a single original model, do NOT pollute list with new STL
|
||||||
target_file_id = pieces[0]['file_id']
|
target_file_id = pieces[0]['file_id']
|
||||||
print_file = PrintFile.query.get(target_file_id)
|
print_file = PrintFile.query.get(target_file_id)
|
||||||
|
|||||||
171
app/tasks.py
171
app/tasks.py
@@ -2,10 +2,14 @@ from huey import SqliteHuey
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
from .models import db, PrintFile, SystemConfig
|
from .models import db, PrintFile, SystemConfig
|
||||||
|
from .conf_parse import ConfParse
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
huey = SqliteHuey(filename='huey_queue.db')
|
huey = SqliteHuey(filename='huey_queue.db')
|
||||||
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
@huey.task()
|
@huey.task()
|
||||||
def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False):
|
def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False):
|
||||||
@@ -27,6 +31,8 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=No
|
|||||||
# Remove DB session to avoid locking the sqlite db during long slicing operations
|
# Remove DB session to avoid locking the sqlite db during long slicing operations
|
||||||
db.session.remove()
|
db.session.remove()
|
||||||
|
|
||||||
|
tmp_def_path = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create Cura engine options
|
# Create Cura engine options
|
||||||
# use our local minimal configurations detached from the entire Cura framework
|
# use our local minimal configurations detached from the entire Cura framework
|
||||||
@@ -34,48 +40,142 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=No
|
|||||||
printers_path = os.path.join(print_config_path, 'printers')
|
printers_path = os.path.join(print_config_path, 'printers')
|
||||||
extruders_path = os.path.join(print_config_path, 'extruders')
|
extruders_path = os.path.join(print_config_path, 'extruders')
|
||||||
materials_path = os.path.join(print_config_path, 'materials')
|
materials_path = os.path.join(print_config_path, 'materials')
|
||||||
presets_path = os.path.join(print_config_path, 'presets')
|
presets_path = os.path.join(print_config_path, 'quality')
|
||||||
|
variants_path = os.path.join(print_config_path, 'variants')
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["CURA_ENGINE_SEARCH_PATH"] = f"{printers_path}:{extruders_path}:{materials_path}:{presets_path}"
|
env["CURA_ENGINE_SEARCH_PATH"] = f"{printers_path}:{extruders_path}:{materials_path}:{presets_path}:{variants_path}"
|
||||||
|
|
||||||
|
def_files = [
|
||||||
|
os.path.join(printers_path, "fdmprinter.def.json"),
|
||||||
|
os.path.join(printers_path, "fdmextruder.def.json"),
|
||||||
|
os.path.join(printers_path, "creality_base.def.json"),
|
||||||
|
os.path.join(printers_path, "creality_ender3v3se.def.json")
|
||||||
|
]
|
||||||
|
|
||||||
|
inst_files_list = []
|
||||||
|
|
||||||
|
if quality_preset:
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
preset_path = os.path.join(presets_path, 'creality', 'presets', quality_preset)
|
||||||
|
if os.path.exists(preset_path):
|
||||||
|
config.read(preset_path)
|
||||||
|
material_type = config.get('metadata', 'material', fallback=None)
|
||||||
|
variant_type = config.get('metadata', 'variant', fallback=None)
|
||||||
|
quality_type = config.get('metadata', 'quality_type', fallback=None)
|
||||||
|
|
||||||
|
if material_type:
|
||||||
|
m_path = os.path.join(materials_path, f"{material_type}.inst.cfg")
|
||||||
|
if os.path.exists(m_path): inst_files_list.append(m_path)
|
||||||
|
if variant_type:
|
||||||
|
variant_d = variant_type.split("mm")[0]
|
||||||
|
v_path = os.path.join(variants_path, "creality", f"creality_ender3v3se_{variant_d}.inst.cfg")
|
||||||
|
if os.path.exists(v_path): inst_files_list.append(v_path)
|
||||||
|
|
||||||
|
if support_pattern == 'tree':
|
||||||
|
t_path = os.path.join(print_config_path, 'supports', 'tree.inst.cfg')
|
||||||
|
if os.path.exists(t_path): inst_files_list.append(t_path)
|
||||||
|
elif support_pattern and support_pattern != 'false':
|
||||||
|
n_path = os.path.join(print_config_path, 'supports', 'normal.inst.cfg')
|
||||||
|
if os.path.exists(n_path): inst_files_list.append(n_path)
|
||||||
|
|
||||||
|
if quality_preset and quality_type:
|
||||||
|
g_path = os.path.join(presets_path, 'creality', 'globals', f"{quality_type}.inst.cfg")
|
||||||
|
if os.path.exists(g_path): inst_files_list.append(g_path)
|
||||||
|
|
||||||
|
if quality_preset and os.path.exists(preset_path):
|
||||||
|
inst_files_list.append(preset_path)
|
||||||
|
|
||||||
|
|
||||||
|
p = ConfParse(def_files)
|
||||||
|
settings_with_inst = p.add_inst_cfg(inst_files_list)
|
||||||
|
|
||||||
|
if infill_density is not None:
|
||||||
|
if "infill_sparse_density" not in settings_with_inst: settings_with_inst["infill_sparse_density"] = {}
|
||||||
|
settings_with_inst["infill_sparse_density"]["value"] = str(infill_density)
|
||||||
|
if "infill_line_distance" not in settings_with_inst: settings_with_inst["infill_line_distance"] = {}
|
||||||
|
settings_with_inst["infill_line_distance"]["value"] = str(100 / int(infill_density)) if int(infill_density) > 0 else "9999"
|
||||||
|
|
||||||
|
if support_enable is not None:
|
||||||
|
if "support_enable" not in settings_with_inst: settings_with_inst["support_enable"] = {}
|
||||||
|
settings_with_inst["support_enable"]["value"] = True if support_enable in ['true', 'buildplate'] else False
|
||||||
|
if "support_type" not in settings_with_inst: settings_with_inst["support_type"] = {}
|
||||||
|
settings_with_inst["support_type"]["value"] = "'buildplate'" if support_enable == 'buildplate' else "'everywhere'"
|
||||||
|
|
||||||
|
if support_pattern == 'tree':
|
||||||
|
if "support_structure" not in settings_with_inst: settings_with_inst["support_structure"] = {}
|
||||||
|
settings_with_inst["support_structure"]["value"] = "'tree'"
|
||||||
|
elif support_pattern in settings_with_inst["support_pattern"]["options"].keys():
|
||||||
|
if "support_structure" not in settings_with_inst: settings_with_inst["support_structure"] = {}
|
||||||
|
settings_with_inst["support_structure"]["value"] = "'normal'"
|
||||||
|
if "support_pattern" not in settings_with_inst: settings_with_inst["support_pattern"] = {}
|
||||||
|
settings_with_inst["support_pattern"]["value"] = f"'{support_pattern}'"
|
||||||
|
|
||||||
|
# Parse to exact values
|
||||||
|
res = p.parse_configs(settings_with_inst)
|
||||||
|
|
||||||
|
override_dict = {}
|
||||||
|
for k, v in res.items():
|
||||||
|
if v.get("enabled", True):
|
||||||
|
val = v.get("value", None)
|
||||||
|
if val is not None:
|
||||||
|
# Filter out our protective ConfigStr wrappers
|
||||||
|
# if type(val).__name__ == "ConfigStr": pass
|
||||||
|
# else: override_dict[k] = {"default_value": val}
|
||||||
|
override_dict[k] = {"value": val,"default_value": val}
|
||||||
|
elif "default_value" in v:
|
||||||
|
override_dict[k] = {"default_value": v["default_value"], "value": v["default_value"]}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
tmp_def_filename = f"tmp_{uuid.uuid4().hex}.def.json"
|
||||||
|
tmp_def_path = os.path.join(app.config['UPLOAD_FOLDER'], tmp_def_filename)
|
||||||
|
|
||||||
|
tmp_def_obj = {
|
||||||
|
"version": 2,
|
||||||
|
"name": "TempProfile",
|
||||||
|
"inherits": "fdmprinter",
|
||||||
|
"metadata": {
|
||||||
|
"visible": True,
|
||||||
|
"author": "System",
|
||||||
|
"manufacturer": "System",
|
||||||
|
"file_formats": "text/x-gcode",
|
||||||
|
"first_start_actions": ["MachineSettingsAction"],
|
||||||
|
"has_materials": True,
|
||||||
|
"has_variants": True,
|
||||||
|
"has_machine_quality": True,
|
||||||
|
"variants_name": "Nozzle Size",
|
||||||
|
|
||||||
|
"preferred_variant_name": "0.4mm Nozzle",
|
||||||
|
"preferred_quality_type": "standard",
|
||||||
|
"preferred_material": "generic_pla",
|
||||||
|
|
||||||
|
},
|
||||||
|
"overrides": override_dict
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty_json = json.dumps(tmp_def_obj, indent=4)
|
||||||
|
|
||||||
|
with open(tmp_def_path, "w") as f:
|
||||||
|
f.write(pretty_json)
|
||||||
|
|
||||||
command = [
|
command = [
|
||||||
"CuraEngine", "slice",
|
"CuraEngine", "slice",
|
||||||
"-j", os.path.join(printers_path, "creality_ender3v3se.def.json")
|
"-j", tmp_def_path,
|
||||||
]
|
|
||||||
|
|
||||||
# Apply quality presets if any
|
|
||||||
if quality_preset:
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
preset_path = os.path.join(presets_path, 'creality', 'base', quality_preset)
|
|
||||||
if os.path.exists(preset_path):
|
|
||||||
config.read(preset_path)
|
|
||||||
if config.has_section('values'):
|
|
||||||
for key, val in config.items('values'):
|
|
||||||
command.extend(['-s', f"{key}={val}"])
|
|
||||||
|
|
||||||
if infill_density is not None:
|
|
||||||
command.extend(['-s', f"infill_sparse_density={infill_density}"])
|
|
||||||
command.extend(['-s', f"infill_line_distance={100 / int(infill_density) if int(infill_density) > 0 else 9999}"])
|
|
||||||
|
|
||||||
if support_enable is not None:
|
|
||||||
command.extend(['-s', f"support_enable={'true' if support_enable == 'true' or support_enable == 'buildplate' else 'false'}"])
|
|
||||||
command.extend(['-s', f"support_type={'buildplate' if support_enable == 'buildplate' else 'everywhere'}"])
|
|
||||||
if support_pattern == 'tree':
|
|
||||||
command.extend(['-s', 'support_structure=tree'])
|
|
||||||
command.extend(['-s', 'support_tree_enable=true'])
|
|
||||||
elif support_pattern and support_pattern != 'false':
|
|
||||||
command.extend(['-s', 'support_structure=normal'])
|
|
||||||
command.extend(['-s', f'support_pattern={support_pattern}'])
|
|
||||||
|
|
||||||
command.extend([
|
|
||||||
"-l", stl_filepath,
|
"-l", stl_filepath,
|
||||||
"-o", gcode_filepath
|
"-o", gcode_filepath
|
||||||
])
|
]
|
||||||
|
|
||||||
app.logger.info(f"Running command: {' '.join(command)}")
|
app.logger.info(f"Running command: {' '.join(command)}")
|
||||||
|
# print(f"Running command: {' '.join(command)}")
|
||||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
|
|
||||||
|
# if stdout:
|
||||||
|
# print(f"[CuraEngine STDOUT]\n{stdout.decode('utf-8', errors='ignore')}")
|
||||||
|
# if stderr:
|
||||||
|
# print(f"[CuraEngine STDERR]\n{stderr.decode('utf-8', errors='ignore')}", flush=True)
|
||||||
|
|
||||||
# Re-fetch print_file and update status
|
# Re-fetch print_file and update status
|
||||||
print_file = PrintFile.query.get(file_id)
|
print_file = PrintFile.query.get(file_id)
|
||||||
if not print_file:
|
if not print_file:
|
||||||
@@ -100,6 +200,13 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=No
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Failed to delete temp STL {stl_filepath}: {e}")
|
app.logger.error(f"Failed to delete temp STL {stl_filepath}: {e}")
|
||||||
|
|
||||||
|
if tmp_def_path and os.path.exists(tmp_def_path):
|
||||||
|
try:
|
||||||
|
os.remove(tmp_def_path)
|
||||||
|
# pass
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Failed to delete temp JSON config {tmp_def_path}: {e}")
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.remove()
|
db.session.remove()
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,69 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2">System Settings</h1>
|
<h1 class="h2">{{ _('System Settings') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5>CuraEngine Configurations</h5>
|
<h5>{{ _('CuraEngine Configurations') }}</h5>
|
||||||
<hr>
|
<hr>
|
||||||
<form method="POST" action="{{ url_for('admin.settings') }}">
|
<form method="POST" action="{{ url_for('admin.settings') }}">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="offset_x" class="form-label">Plater Origin Offset X (mm)</label>
|
<label for="offset_x" class="form-label">{{ _('Plater Origin Offset X (mm)') }}</label>
|
||||||
<input type="number" class="form-control" name="offset_x" id="offset_x" value="{{ configs.get('offset_x', '0') }}">
|
<input type="number" class="form-control" name="offset_x" id="offset_x" value="{{ configs.get('offset_x', '0') }}">
|
||||||
<div class="form-text">Adjust the X-axis compilation offset for combined files on the build plate.</div>
|
<div class="form-text">{{ _('Adjust the X-axis compilation offset for combined files on the build plate.') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="offset_y" class="form-label">Plater Origin Offset Y (mm)</label>
|
<label for="offset_y" class="form-label">{{ _('Plater Origin Offset Y (mm)') }}</label>
|
||||||
<input type="number" class="form-control" name="offset_y" id="offset_y" value="{{ configs.get('offset_y', '0') }}">
|
<input type="number" class="form-control" name="offset_y" id="offset_y" value="{{ configs.get('offset_y', '0') }}">
|
||||||
<div class="form-text">Adjust the Y-axis compilation offset for combined files on the build plate.</div>
|
<div class="form-text">{{ _('Adjust the Y-axis compilation offset for combined files on the build plate.') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
<h5 class="mt-4">{{ _('Default Plater Settings') }}</h5>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="default_infill" class="form-label">{{ _('Default Infill Density (%)') }}</label>
|
||||||
|
<input type="number" class="form-control" name="default_infill" id="default_infill" value="{{ configs.get('default_infill', '20') }}" min="0" max="100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="default_support" class="form-label">{{ _('Default Support') }}</label>
|
||||||
|
<select class="form-select" name="default_support" id="default_support">
|
||||||
|
<option value="false" {% if configs.get('default_support', 'false') == 'false' %}selected{% endif %}>{{ _('None') }}</option>
|
||||||
|
<option value="buildplate" {% if configs.get('default_support', 'false') == 'buildplate' %}selected{% endif %}>{{ _('Touching Buildplate') }}</option>
|
||||||
|
<option value="true" {% if configs.get('default_support', 'false') == 'true' %}selected{% endif %}>{{ _('Everywhere') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="default_support_pattern" class="form-label">{{ _('Default Support Type') }}</label>
|
||||||
|
<select class="form-select" name="default_support_pattern" id="default_support_pattern">
|
||||||
|
<option value="tree" {% if configs.get('default_support_pattern', 'tree') == 'tree' %}selected{% endif %}>{{ _('Tree') }}</option>
|
||||||
|
<option value="lines" {% if configs.get('default_support_pattern', 'tree') == 'lines' %}selected{% endif %}>{{ _('Lines') }}</option>
|
||||||
|
<option value="grid" {% if configs.get('default_support_pattern', 'tree') == 'grid' %}selected{% endif %}>{{ _('Grid') }}</option>
|
||||||
|
<option value="triangles" {% if configs.get('default_support_pattern', 'tree') == 'triangles' %}selected{% endif %}>{{ _('Triangles') }}</option>
|
||||||
|
<option value="concentric" {% if configs.get('default_support_pattern', 'tree') == 'concentric' %}selected{% endif %}>{{ _('Concentric') }}</option>
|
||||||
|
<option value="zigzag" {% if configs.get('default_support_pattern', 'tree') == 'zigzag' %}selected{% endif %}>{{ _('Zig Zag') }}</option>
|
||||||
|
<option value="cross" {% if configs.get('default_support_pattern', 'tree') == 'cross' %}selected{% endif %}>{{ _('Cross') }}</option>
|
||||||
|
<option value="gyroid" {% if configs.get('default_support_pattern', 'tree') == 'gyroid' %}selected{% endif %}>{{ _('Gyroid') }}</option>
|
||||||
|
<option value="honeycomb" {% if configs.get('default_support_pattern', 'tree') == 'honeycomb' %}selected{% endif %}>{{ _('Honeycomb') }}</option>
|
||||||
|
<option value="octagon" {% if configs.get('default_support_pattern', 'tree') == 'octagon' %}selected{% endif %}>{{ _('Octagon') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="default_quality" class="form-label">{{ _('Default Quality Profile') }}</label>
|
||||||
|
<select class="form-select" name="default_quality" id="default_quality">
|
||||||
|
{% for key, name in presets %}
|
||||||
|
<option value="{{ key }}" {% if configs.get('default_quality', 'base_global_standard.inst.cfg') == key %}selected{% endif %}>{{ _(name) }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">{{ _('Save Settings') }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2">User Management</h1>
|
<h1 class="h2">{{ _('User Management') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>{{ _('ID') }}</th>
|
||||||
<th>Username</th>
|
<th>{{ _('Username') }}</th>
|
||||||
<th>Role</th>
|
<th>{{ _('Role') }}</th>
|
||||||
<th>Created At</th>
|
<th>{{ _('Created At') }}</th>
|
||||||
<th>Actions</th>
|
<th>{{ _('Actions') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -23,16 +23,18 @@
|
|||||||
<td>{{ user.username }}</td>
|
<td>{{ user.username }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<span class="badge bg-danger">Admin</span>
|
<span class="badge bg-danger">{{ _('Admin') }}</span>
|
||||||
{% elif user.is_guest %}
|
{% elif user.is_guest %}
|
||||||
<span class="badge bg-secondary">Guest</span>
|
<span class="badge bg-secondary">{{ _('Guest') }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-primary">User</span>
|
<span class="badge bg-primary">{{ _('User') }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>Delete</button>
|
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ _('WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?') }}');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>{{ _('Delete') }}</button>
|
||||||
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -2,21 +2,470 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2">GCode Preview: {{ file.original_filename }}</h1>
|
<h1 class="h2"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
|
||||||
<a href="{{ url_for('main.files') }}" class="btn btn-secondary btn-sm">Back to Files</a>
|
<div>
|
||||||
|
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
|
||||||
|
<a href="{{ url_for('main.files') }}" class="btn btn-outline-secondary btn-sm rounded ms-2 shadow-sm"><i class="bi bi-arrow-left"></i> {{ _('Back') }}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-4">
|
<div id="loading-overlay" class="text-center py-5 my-5">
|
||||||
<div class="card-header bg-info text-dark d-flex justify-content-between">
|
<div class="spinner-border text-primary shadow-sm" role="status" style="width: 3rem; height: 3rem;"></div>
|
||||||
<span>File Info</span>
|
<h4 class="mt-4 text-secondary">{{ _('Loading and Parsing GCode Data...') }}</h4>
|
||||||
<span>Total Lines: {{ line_count }}</span>
|
</div>
|
||||||
|
|
||||||
|
<div class="row d-none" id="preview-container" style="height: 75vh;">
|
||||||
|
<!-- 3D Canvas Area -->
|
||||||
|
<div class="col-md-11 position-relative h-100 p-0 border rounded border-secondary shadow-sm" style="background: #111;">
|
||||||
|
<div id="canvas-container" class="w-100 h-100 d-block overflow-hidden"></div>
|
||||||
|
|
||||||
|
<!-- Legend Overlay -->
|
||||||
|
<div class="position-absolute top-0 start-0 m-3 p-2 rounded shadow bg-dark bg-opacity-75 border border-secondary" style="color: #eee; font-size: 0.85rem; pointer-events: auto; z-index: 10;">
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="WALL-OUTER" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #eb8b38;"></span>{{ _('Outer Wall') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="WALL-INNER" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #4080cf;"></span>{{ _('Inner Wall') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="FILL" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #ccc04b;"></span>{{ _('Infill') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="SKIN" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #9e60b3;"></span>{{ _('Skin/TopBottom') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="SUPPORT" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #57b357;"></span>{{ _('Support') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="SKIRT" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #00ffff;"></span>{{ _('Skirt') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="SUPPORT-INTERFACE" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #2b6b2b;"></span>{{ _('Support Interface') }}</div>
|
||||||
|
<div class="mb-1 legend-item user-select-none" data-type="TRAVEL" style="cursor: pointer; transition: opacity 0.2s;"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: #405060;"></span>{{ _('Travel (Move)') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Slider (Intra-Layer Progress) -->
|
||||||
|
<div class="position-absolute bottom-0 start-0 w-100 p-3" style="background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.8) 100%); z-index: 10;">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="text-white fw-medium text-nowrap user-select-none"><i class="bi bi-play-circle me-1"></i>{{ _('Layer Progress:') }}</span>
|
||||||
|
<input type="range" class="form-range flex-grow-1" id="progress-slider" min="0" max="100" value="100" step="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
|
||||||
<p class="card-text text-muted mb-1">Below is a text preview of the generated GCode (first 500 lines).</p>
|
<!-- Right Sidebar (Layer Slider) -->
|
||||||
<pre class="bg-dark text-light p-3 rounded" style="max-height: 500px; overflow-y: auto; font-size: 13px;"><code>{{ content }}</code></pre>
|
<div class="col-md-1 h-100 d-flex flex-column align-items-center justify-content-center bg-white border rounded shadow-sm position-relative">
|
||||||
</div>
|
<label class="form-label mb-3 fw-bold text-center text-primary mt-3">{{ _('Layer') }}<br>
|
||||||
<div class="card-footer">
|
<span id="layer-display" class="badge bg-primary fs-6 mt-1 shadow-sm px-3 rounded-pill">0</span>
|
||||||
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary">Download Full GCode File</a>
|
</label>
|
||||||
|
<div class="flex-grow-1 w-100 d-flex justify-content-center pb-4 py-2">
|
||||||
|
<input type="range" class="form-range h-100" id="layer-slider" min="0" max="0" value="0" style="writing-mode: bt-lr; -webkit-appearance: slider-vertical; cursor: ns-resize;">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/three.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/OrbitControls.js') }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const COLORS = {
|
||||||
|
'WALL-OUTER': new THREE.Color(0xeb8b38),
|
||||||
|
'WALL-INNER': new THREE.Color(0x4080cf),
|
||||||
|
'FILL': new THREE.Color(0xccc04b),
|
||||||
|
'SKIN': new THREE.Color(0x9e60b3),
|
||||||
|
'SUPPORT': new THREE.Color(0x57b357),
|
||||||
|
'SKIRT': new THREE.Color(0x00ffff),
|
||||||
|
'SUPPORT-INTERFACE': new THREE.Color(0x2b6b2b),
|
||||||
|
'TRAVEL': new THREE.Color(0x405060),
|
||||||
|
'DEFAULT': new THREE.Color(0xaaaaaa),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inject printer machine dimensions via Jinja
|
||||||
|
const bedWidth = {{ machine_width | default(220) }};
|
||||||
|
const bedDepth = {{ machine_depth | default(220) }};
|
||||||
|
const bedHeight = {{ machine_height | default(250) }};
|
||||||
|
const offsetX = {{ offset_x | default(0.0) }};
|
||||||
|
const offsetY = {{ offset_y | default(0.0) }};
|
||||||
|
|
||||||
|
// Type indices for shader visibility filtering
|
||||||
|
const TYPE_INDEX = {
|
||||||
|
'TRAVEL': 0, 'WALL-OUTER': 1, 'WALL-INNER': 2,
|
||||||
|
'FILL': 3, 'SKIN': 4, 'SUPPORT': 5, 'DEFAULT': 6,
|
||||||
|
'SKIRT': 7, 'SUPPORT-INTERFACE': 8
|
||||||
|
};
|
||||||
|
|
||||||
|
let layers = [];
|
||||||
|
let scene, camera, renderer, controls;
|
||||||
|
let group = new THREE.Group();
|
||||||
|
|
||||||
|
const layerSlider = document.getElementById('layer-slider');
|
||||||
|
const layerDisplay = document.getElementById('layer-display');
|
||||||
|
const progressSlider = document.getElementById('progress-slider');
|
||||||
|
|
||||||
|
// Shader material for high-speed dynamic feature visibility
|
||||||
|
const gcodeMat = new THREE.ShaderMaterial({
|
||||||
|
uniforms: {
|
||||||
|
uShowOuter: { value: 1.0 },
|
||||||
|
uShowInner: { value: 1.0 },
|
||||||
|
uShowInfill: { value: 1.0 },
|
||||||
|
uShowSkin: { value: 1.0 },
|
||||||
|
uShowSupport: { value: 1.0 },
|
||||||
|
uShowSkirt: { value: 1.0 },
|
||||||
|
uShowSupportInterface: { value: 1.0 },
|
||||||
|
uShowTravel: { value: 1.0 },
|
||||||
|
uShowDefault: { value: 1.0 }
|
||||||
|
},
|
||||||
|
vertexShader: `
|
||||||
|
attribute float pType;
|
||||||
|
varying vec3 vColor;
|
||||||
|
varying float vType;
|
||||||
|
void main() {
|
||||||
|
vColor = color;
|
||||||
|
vType = pType;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
fragmentShader: `
|
||||||
|
varying vec3 vColor;
|
||||||
|
varying float vType;
|
||||||
|
uniform float uShowOuter;
|
||||||
|
uniform float uShowInner;
|
||||||
|
uniform float uShowInfill;
|
||||||
|
uniform float uShowSkin;
|
||||||
|
uniform float uShowSupport;
|
||||||
|
uniform float uShowSkirt;
|
||||||
|
uniform float uShowSupportInterface;
|
||||||
|
uniform float uShowTravel;
|
||||||
|
uniform float uShowDefault;
|
||||||
|
void main() {
|
||||||
|
float show = 1.0;
|
||||||
|
int t = int(vType + 0.5);
|
||||||
|
if (t == 0) show = uShowTravel;
|
||||||
|
else if (t == 1) show = uShowOuter;
|
||||||
|
else if (t == 2) show = uShowInner;
|
||||||
|
else if (t == 3) show = uShowInfill;
|
||||||
|
else if (t == 4) show = uShowSkin;
|
||||||
|
else if (t == 5) show = uShowSupport;
|
||||||
|
else if (t == 7) show = uShowSkirt;
|
||||||
|
else if (t == 8) show = uShowSupportInterface;
|
||||||
|
else show = uShowDefault;
|
||||||
|
|
||||||
|
if (show < 0.5) discard;
|
||||||
|
gl_FragColor = vec4(vColor, 1.0);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
vertexColors: true,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
linewidth: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
// Binding the Legend Buttons
|
||||||
|
const uniformMap = {
|
||||||
|
'WALL-OUTER': 'uShowOuter',
|
||||||
|
'WALL-INNER': 'uShowInner',
|
||||||
|
'FILL': 'uShowInfill',
|
||||||
|
'SKIN': 'uShowSkin',
|
||||||
|
'SUPPORT': 'uShowSupport',
|
||||||
|
'SKIRT': 'uShowSkirt',
|
||||||
|
'SUPPORT-INTERFACE': 'uShowSupportInterface',
|
||||||
|
'TRAVEL': 'uShowTravel'
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll('.legend-item').forEach(el => {
|
||||||
|
el.addEventListener('click', function() {
|
||||||
|
const t = this.dataset.type;
|
||||||
|
const uniformName = uniformMap[t];
|
||||||
|
if (uniformName) {
|
||||||
|
const currentVal = gcodeMat.uniforms[uniformName].value;
|
||||||
|
const newVal = currentVal > 0.5 ? 0.0 : 1.0;
|
||||||
|
gcodeMat.uniforms[uniformName].value = newVal;
|
||||||
|
this.style.opacity = newVal > 0.5 ? "1.0" : "0.4";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function init3D() {
|
||||||
|
const container = document.getElementById('canvas-container');
|
||||||
|
scene = new THREE.Scene();
|
||||||
|
scene.background = new THREE.Color(0x1a1a1a);
|
||||||
|
|
||||||
|
scene.add(group);
|
||||||
|
|
||||||
|
camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 5000);
|
||||||
|
camera.up.set(0, 0, 1);
|
||||||
|
|
||||||
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
container.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = false;
|
||||||
|
controls.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
|
||||||
|
|
||||||
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowResize() {
|
||||||
|
const container = document.getElementById('canvas-container');
|
||||||
|
camera.aspect = container.clientWidth / container.clientHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(container.clientWidth, container.clientHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
controls.update();
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGCode() {
|
||||||
|
try {
|
||||||
|
const url = '{{ url_for("main.download_gcode", file_id=file.id) }}';
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error("GCode Request Failed");
|
||||||
|
|
||||||
|
const gcodeText = await response.text();
|
||||||
|
|
||||||
|
document.getElementById('loading-overlay').classList.add('d-none');
|
||||||
|
document.getElementById('preview-container').classList.remove('d-none');
|
||||||
|
|
||||||
|
init3D();
|
||||||
|
parseGCode(gcodeText);
|
||||||
|
|
||||||
|
// Add grid matching printer size
|
||||||
|
setupMachineEnvironment();
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Init controls
|
||||||
|
layerSlider.max = Math.max(0, layers.length - 1);
|
||||||
|
layerSlider.value = Math.max(0, layers.length - 1);
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
layerSlider.addEventListener('input', updateUI);
|
||||||
|
progressSlider.addEventListener('input', updateUI);
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Error Loading GCode", e);
|
||||||
|
document.getElementById('loading-overlay').innerHTML = `
|
||||||
|
<div class="text-danger my-5 py-5">
|
||||||
|
<i class="bi bi-exclamation-triangle display-1"></i>
|
||||||
|
<h3 class="mt-3">{{ _('Failed to load GCode preview.') }}</h3>
|
||||||
|
<p class="text-muted">${e.toString()}</p>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGCode(text) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let current = { x: 0, y: 0, z: 0, e: 0 };
|
||||||
|
let currentTypeStr = 'DEFAULT';
|
||||||
|
|
||||||
|
let currentExtrudePoints = [];
|
||||||
|
let currentExtrudeColors = [];
|
||||||
|
let currentExtrudeTypes = [];
|
||||||
|
|
||||||
|
let currentTravelPoints = [];
|
||||||
|
let currentTravelColors = [];
|
||||||
|
let currentTravelTypes = [];
|
||||||
|
|
||||||
|
function flushLayer() {
|
||||||
|
if (currentExtrudePoints.length === 0 && currentTravelPoints.length === 0) return;
|
||||||
|
|
||||||
|
let layerGroup = new THREE.Group();
|
||||||
|
|
||||||
|
if (currentExtrudePoints.length > 0) {
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(currentExtrudePoints, 3));
|
||||||
|
geo.setAttribute('color', new THREE.Float32BufferAttribute(currentExtrudeColors, 3));
|
||||||
|
geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentExtrudeTypes, 1));
|
||||||
|
const mesh = new THREE.Mesh(geo, gcodeMat);
|
||||||
|
mesh.userData.isExtrude = true;
|
||||||
|
layerGroup.add(mesh);
|
||||||
|
currentExtrudePoints = []; currentExtrudeColors = []; currentExtrudeTypes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTravelPoints.length > 0) {
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(currentTravelPoints, 3));
|
||||||
|
geo.setAttribute('color', new THREE.Float32BufferAttribute(currentTravelColors, 3));
|
||||||
|
geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentTravelTypes, 1));
|
||||||
|
const lineSeg = new THREE.LineSegments(geo, gcodeMat);
|
||||||
|
lineSeg.userData.isTravel = true;
|
||||||
|
layerGroup.add(lineSeg);
|
||||||
|
currentTravelPoints = []; currentTravelColors = []; currentTravelTypes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.push(layerGroup);
|
||||||
|
group.add(layerGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let chunk = lines[i].trim().toUpperCase();
|
||||||
|
if (!chunk) continue;
|
||||||
|
|
||||||
|
if (chunk.startsWith(';LAYER:')) {
|
||||||
|
flushLayer();
|
||||||
|
} else if (chunk.startsWith(';TYPE:')) {
|
||||||
|
currentTypeStr = chunk.substring(6).trim();
|
||||||
|
} else if (chunk.startsWith('G0') || chunk.startsWith('G1')) {
|
||||||
|
let next = { x: current.x, y: current.y, z: current.z, e: current.e };
|
||||||
|
let parts = chunk.split(/\s+/);
|
||||||
|
let hasMove = false;
|
||||||
|
|
||||||
|
for (let p of parts) {
|
||||||
|
if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.x = v; hasMove = true; } }
|
||||||
|
if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.y = v; hasMove = true; } }
|
||||||
|
if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.z = v; hasMove = true; } }
|
||||||
|
if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.e = v; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) {
|
||||||
|
let isExtrude = (next.e > current.e);
|
||||||
|
// Cura uses G0 for travel generally
|
||||||
|
if (chunk.startsWith('G0') && !chunk.includes('E')) isExtrude = false;
|
||||||
|
|
||||||
|
let activeType = isExtrude ? currentTypeStr : 'TRAVEL';
|
||||||
|
let col = COLORS[activeType] || COLORS['DEFAULT'];
|
||||||
|
let tIdx = TYPE_INDEX[activeType] !== undefined ? TYPE_INDEX[activeType] : TYPE_INDEX['DEFAULT'];
|
||||||
|
|
||||||
|
if (isExtrude) {
|
||||||
|
let dx = next.x - current.x;
|
||||||
|
let dy = next.y - current.y;
|
||||||
|
let dist = Math.sqrt(dx*dx + dy*dy);
|
||||||
|
if (dist > 0.0001) {
|
||||||
|
let hw = 0.4 / 2.0; // 0.4mm wire width
|
||||||
|
let hh = 0.2 / 2.0; // 0.2mm layer height roughly
|
||||||
|
|
||||||
|
let nx = -(dy / dist) * hw;
|
||||||
|
let ny = (dx / dist) * hw;
|
||||||
|
|
||||||
|
let p1x = current.x + nx, p1y = current.y + ny; // current-left
|
||||||
|
let p2x = current.x - nx, p2y = current.y - ny; // current-right
|
||||||
|
let p3x = next.x + nx, p3y = next.y + ny; // next-left
|
||||||
|
let p4x = next.x - nx, p4y = next.y - ny; // next-right
|
||||||
|
|
||||||
|
// Top face
|
||||||
|
currentExtrudePoints.push(
|
||||||
|
p1x, p1y, current.z + hh,
|
||||||
|
p3x, p3y, next.z + hh,
|
||||||
|
p2x, p2y, current.z + hh,
|
||||||
|
p3x, p3y, next.z + hh,
|
||||||
|
p4x, p4y, next.z + hh,
|
||||||
|
p2x, p2y, current.z + hh
|
||||||
|
);
|
||||||
|
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r, col.g, col.b); currentExtrudeTypes.push(tIdx); }
|
||||||
|
|
||||||
|
// Bottom face
|
||||||
|
currentExtrudePoints.push(
|
||||||
|
p1x, p1y, current.z - hh,
|
||||||
|
p2x, p2y, current.z - hh,
|
||||||
|
p3x, p3y, next.z - hh,
|
||||||
|
p2x, p2y, current.z - hh,
|
||||||
|
p4x, p4y, next.z - hh,
|
||||||
|
p3x, p3y, next.z - hh
|
||||||
|
);
|
||||||
|
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.4, col.g*0.4, col.b*0.4); currentExtrudeTypes.push(tIdx); }
|
||||||
|
|
||||||
|
// Left face
|
||||||
|
currentExtrudePoints.push(
|
||||||
|
p1x, p1y, current.z - hh,
|
||||||
|
p3x, p3y, next.z - hh,
|
||||||
|
p1x, p1y, current.z + hh,
|
||||||
|
p3x, p3y, next.z - hh,
|
||||||
|
p3x, p3y, next.z + hh,
|
||||||
|
p1x, p1y, current.z + hh
|
||||||
|
);
|
||||||
|
// Fake lighting based on normal side
|
||||||
|
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.6, col.g*0.6, col.b*0.6); currentExtrudeTypes.push(tIdx); }
|
||||||
|
|
||||||
|
// Right face
|
||||||
|
currentExtrudePoints.push(
|
||||||
|
p2x, p2y, current.z - hh,
|
||||||
|
p2x, p2y, current.z + hh,
|
||||||
|
p4x, p4y, next.z - hh,
|
||||||
|
p2x, p2y, current.z + hh,
|
||||||
|
p4x, p4y, next.z + hh,
|
||||||
|
p4x, p4y, next.z - hh
|
||||||
|
);
|
||||||
|
for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.8, col.g*0.8, col.b*0.8); currentExtrudeTypes.push(tIdx); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentTravelPoints.push(current.x, current.y, current.z);
|
||||||
|
currentTravelPoints.push(next.x, next.y, next.z);
|
||||||
|
currentTravelColors.push(col.r, col.g, col.b, col.r, col.g, col.b);
|
||||||
|
currentTravelTypes.push(tIdx, tIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
current.x = next.x; current.y = next.y; current.z = next.z; current.e = next.e;
|
||||||
|
}
|
||||||
|
} else if (chunk.startsWith('G92')) {
|
||||||
|
let parts = chunk.split(/\s+/);
|
||||||
|
for (let p of parts) {
|
||||||
|
if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.e = v; }
|
||||||
|
if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.x = v; }
|
||||||
|
if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.y = v; }
|
||||||
|
if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.z = v; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMachineEnvironment() {
|
||||||
|
if (layers.length === 0) return;
|
||||||
|
|
||||||
|
let bbox = new THREE.Box3();
|
||||||
|
for (let layerGrp of layers) {
|
||||||
|
layerGrp.children.forEach(child => {
|
||||||
|
child.geometry.computeBoundingBox();
|
||||||
|
bbox.union(child.geometry.boundingBox);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The GCode coordinates for the actual print bed are from (0,0) to (W,H).
|
||||||
|
// The GCode trajectory is ALREADY offset by plater.html during slicing.
|
||||||
|
// We just need to place the grid exactly in the center of the bed: (W/2, H/2).
|
||||||
|
let gridOffsetX = (bedWidth / 2);
|
||||||
|
let gridOffsetY = (bedDepth / 2);
|
||||||
|
|
||||||
|
// Add Grid
|
||||||
|
const gridDivisions = Math.ceil(Math.max(bedWidth, bedDepth) / 10);
|
||||||
|
const gridHelper = new THREE.GridHelper(Math.max(bedWidth, bedDepth), gridDivisions, 0x444444, 0x242424);
|
||||||
|
gridHelper.rotation.x = Math.PI / 2;
|
||||||
|
gridHelper.position.set(gridOffsetX, gridOffsetY, 0);
|
||||||
|
scene.add(gridHelper);
|
||||||
|
|
||||||
|
// Add Printer Volume Outline
|
||||||
|
const boxGeo = new THREE.BoxGeometry(bedWidth, bedDepth, bedHeight);
|
||||||
|
const edges = new THREE.EdgesGeometry(boxGeo);
|
||||||
|
const boxOutline = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x444444 }));
|
||||||
|
boxOutline.position.set(gridOffsetX, gridOffsetY, bedHeight/2);
|
||||||
|
scene.add(boxOutline);
|
||||||
|
|
||||||
|
// Align Camera to target the center of the bed grid
|
||||||
|
controls.target.set(gridOffsetX, gridOffsetY, 0);
|
||||||
|
camera.position.set(gridOffsetX, gridOffsetY - (bedDepth * 1.5), bedHeight * 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (layers.length === 0) return;
|
||||||
|
let activeIdx = parseInt(layerSlider.value);
|
||||||
|
let intraProg = parseFloat(progressSlider.value);
|
||||||
|
|
||||||
|
layerDisplay.innerText = activeIdx + " / " + (layers.length - 1);
|
||||||
|
|
||||||
|
for (let i = 0; i < layers.length; i++) {
|
||||||
|
let layerGrp = layers[i];
|
||||||
|
if (i < activeIdx) {
|
||||||
|
layerGrp.visible = true;
|
||||||
|
layerGrp.children.forEach(child => child.geometry.setDrawRange(0, Infinity));
|
||||||
|
} else if (i === activeIdx) {
|
||||||
|
layerGrp.visible = true;
|
||||||
|
layerGrp.children.forEach(child => {
|
||||||
|
let totalVertices = child.geometry.attributes.position.count;
|
||||||
|
let elementsPerUnit = child.userData.isTravel ? 2 : 24;
|
||||||
|
let totalUnits = totalVertices / elementsPerUnit;
|
||||||
|
let drawCount = Math.floor(totalUnits * (intraProg / 100)) * elementsPerUnit;
|
||||||
|
child.geometry.setDrawRange(0, drawCount);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
layerGrp.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGCode();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,17 +1,46 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
/* 防止整个大页面滚动 */
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<h1 class="h2"><i class="bi bi-grid-3x3 me-2 text-primary"></i>{{ _('Plater / Build Plate') }}</h1>
|
<h1 class="h2"><i class="bi bi-grid-3x3 me-2 text-primary"></i>{{ _('Plater / Build Plate') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" style="height: 70vh;">
|
<div class="row" style="height: calc(100vh - 140px);">
|
||||||
|
|
||||||
|
|
||||||
<!-- 3D Area -->
|
<!-- 3D Area -->
|
||||||
<div class="col-md-9 h-100 position-relative">
|
<div class="col-md-9 h-100 position-relative">
|
||||||
<div id="plater-container" class="w-100 h-100 rounded shadow-sm border border-secondary" style="overflow: hidden; background: #f8f9fa;"></div>
|
<div id="plater-container" class="w-100 h-100 rounded shadow-sm border border-secondary" style="overflow: hidden; background: #f8f9fa;"></div>
|
||||||
|
|
||||||
|
<!-- Parameterized Scale Input Box -->
|
||||||
|
<div id="scale-panel" class="position-absolute top-50 start-0 translate-middle-y ms-5 ps-4 d-none" style="z-index: 10; pointer-events: none;">
|
||||||
|
<div class="bg-white rounded shadow-sm p-3 opacity-90 border border-secondary" style="width: 170px; pointer-events: auto;">
|
||||||
|
<h6 class="fs-6 mb-2 text-primary"><i class="bi bi-arrows-angle-expand me-1"></i>{{ _('Scale') }}</h6>
|
||||||
|
<div class="input-group input-group-sm mb-1">
|
||||||
|
<span class="input-group-text bg-danger text-white border-danger fw-bold opacity-75" style="width: 32px;">X</span>
|
||||||
|
<input type="number" class="form-control" id="scale-x" value="1.0" step="0.1" onchange="applyScaleInput('x')">
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm mb-1">
|
||||||
|
<span class="input-group-text bg-success text-white border-success fw-bold opacity-75" style="width: 32px;">Y</span>
|
||||||
|
<input type="number" class="form-control" id="scale-y" value="1.0" step="0.1" onchange="applyScaleInput('y')">
|
||||||
|
</div>
|
||||||
|
<div class="input-group input-group-sm mb-2">
|
||||||
|
<span class="input-group-text bg-primary text-white border-primary fw-bold opacity-75" style="width: 32px;">Z</span>
|
||||||
|
<input type="number" class="form-control" id="scale-z" value="1.0" step="0.1" onchange="applyScaleInput('z')">
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch small mb-0 mt-1">
|
||||||
|
<input class="form-check-input" type="checkbox" id="scale-uniform" checked>
|
||||||
|
<label class="form-check-label user-select-none" for="scale-uniform">{{ _('Uniform Scale') }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="position-absolute top-50 start-0 translate-middle-y ms-3 p-2 bg-white rounded shadow-sm d-flex flex-column gap-2 opacity-75" style="z-index: 10;">
|
<div class="position-absolute top-50 start-0 translate-middle-y ms-3 p-2 bg-white rounded shadow-sm d-flex flex-column gap-2 opacity-75" style="z-index: 10;">
|
||||||
<button class="btn btn-primary btn-sm rounded" id="btn-translate" title="{{ _('Translate (W)') }}" onclick="setTransformMode('translate')"><i class="bi bi-arrows-move"></i></button>
|
<button class="btn btn-primary btn-sm rounded" id="btn-translate" title="{{ _('Translate (W)') }}" onclick="setTransformMode('translate')"><i class="bi bi-arrows-move"></i></button>
|
||||||
<button class="btn btn-outline-secondary btn-sm rounded" id="btn-rotate" title="{{ _('Rotate (E)') }}" onclick="setTransformMode('rotate')"><i class="bi bi-arrow-clockwise"></i></button>
|
<button class="btn btn-outline-secondary btn-sm rounded" id="btn-rotate" title="{{ _('Rotate (E)') }}" onclick="setTransformMode('rotate')"><i class="bi bi-arrow-clockwise"></i></button>
|
||||||
@@ -30,7 +59,7 @@
|
|||||||
<i class="bi bi-chevron-bar-contract"></i>
|
<i class="bi bi-chevron-bar-contract"></i>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapseModels" class="collapse show">
|
<div id="collapseModels" class="collapse show">
|
||||||
<div class="list-group list-group-flush" id="model-list" style="max-height: 250px; overflow-y: auto;">
|
<div class="list-group list-group-flush" id="model-list" style="min-height: 160px; max-height: max(250px, 35vh); overflow-y: auto;">
|
||||||
{% for model in models %}
|
{% for model in models %}
|
||||||
<button id="add-model-btn-{{ model.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-matrix="{{ model.transform_matrix or '' }}" onclick="addModelToPlate(this, {{ model.id }}, '{{ model.url }}', '{{ model.name }}', '{{ model.status }}')">
|
<button id="add-model-btn-{{ model.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-matrix="{{ model.transform_matrix or '' }}" onclick="addModelToPlate(this, {{ model.id }}, '{{ model.url }}', '{{ model.name }}', '{{ model.status }}')">
|
||||||
<span class="text-truncate">{{ model.name }}</span>
|
<span class="text-truncate">{{ model.name }}</span>
|
||||||
@@ -52,24 +81,29 @@
|
|||||||
<div class="card-body py-2">
|
<div class="card-body py-2">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="infill-density" class="form-label text-secondary small mb-1">{{ _('Infill Density') }} (%)</label>
|
<label for="infill-density" class="form-label text-secondary small mb-1">{{ _('Infill Density') }} (%)</label>
|
||||||
<input type="number" class="form-control form-control-sm" id="infill-density" value="20" min="0" max="100">
|
<input type="number" class="form-control form-control-sm" id="infill-density" value="{{ default_infill }}" min="0" max="100">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="support-type" class="form-label text-secondary small mb-1">{{ _('Support') }}</label>
|
<label for="support-type" class="form-label text-secondary small mb-1">{{ _('Support') }}</label>
|
||||||
<select class="form-select form-select-sm" id="support-type">
|
<select class="form-select form-select-sm" id="support-type">
|
||||||
<option value="false">{{ _('None') }}</option>
|
<option value="false" {% if default_support == 'false' %}selected{% endif %}>{{ _('None') }}</option>
|
||||||
<option value="buildplate">{{ _('Touching Buildplate') }}</option>
|
<option value="buildplate" {% if default_support == 'buildplate' %}selected{% endif %}>{{ _('Touching Buildplate') }}</option>
|
||||||
<option value="true">{{ _('Everywhere') }}</option>
|
<option value="true" {% if default_support == 'true' %}selected{% endif %}>{{ _('Everywhere') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="support-pattern" class="form-label text-secondary small mb-1">{{ _('Support Type') }}</label>
|
<label for="support-pattern" class="form-label text-secondary small mb-1">{{ _('Support Type') }}</label>
|
||||||
<select class="form-select form-select-sm" id="support-pattern" disabled>
|
<select class="form-select form-select-sm" id="support-pattern" {% if default_support == 'false' %}disabled{% endif %}>
|
||||||
<option value="lines">{{ _('Lines') }} ({{ _('默认线状') }})</option>
|
<option value="tree" {% if default_support_pattern == 'tree' %}selected{% endif %}>{{ _('Tree') }}</option>
|
||||||
<option value="grid">{{ _('Grid') }} ({{ _('网格状') }})</option>
|
<option value="lines" {% if default_support_pattern == 'lines' %}selected{% endif %}>{{ _('Lines') }}</option>
|
||||||
<option value="triangles">{{ _('Triangles') }} ({{ _('三角网') }})</option>
|
<option value="grid" {% if default_support_pattern == 'grid' %}selected{% endif %}>{{ _('Grid') }}</option>
|
||||||
<option value="zigzag">{{ _('ZigZag') }} ({{ _('之字形') }})</option>
|
<option value="triangles" {% if default_support_pattern == 'triangles' %}selected{% endif %}>{{ _('Triangles') }}</option>
|
||||||
<option value="tree">{{ _('Tree') }} ({{ _('树状') }})</option>
|
<option value="concentric" {% if default_support_pattern == 'concentric' %}selected{% endif %}>{{ _('Concentric') }}</option>
|
||||||
|
<option value="zigzag" {% if default_support_pattern == 'zigzag' %}selected{% endif %}>{{ _('Zig Zag') }}</option>
|
||||||
|
<option value="cross" {% if default_support_pattern == 'cross' %}selected{% endif %}>{{ _('Cross') }}</option>
|
||||||
|
<option value="gyroid" {% if default_support_pattern == 'gyroid' %}selected{% endif %}>{{ _('Gyroid') }}</option>
|
||||||
|
<option value="honeycomb" {% if default_support_pattern == 'honeycomb' %}selected{% endif %}>{{ _('Honeycomb') }}</option>
|
||||||
|
<option value="octagon" {% if default_support_pattern == 'octagon' %}selected{% endif %}>{{ _('Octagon') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +174,7 @@ let offsetX = {{ offset_x|default(0) }};
|
|||||||
let offsetY = {{ offset_y|default(0) }};
|
let offsetY = {{ offset_y|default(0) }};
|
||||||
let loadedModels = [];
|
let loadedModels = [];
|
||||||
let activeModel = null;
|
let activeModel = null;
|
||||||
|
const initialAddId = new URLSearchParams(window.location.search).get('add');
|
||||||
|
|
||||||
initPlater();
|
initPlater();
|
||||||
animate();
|
animate();
|
||||||
@@ -220,7 +255,8 @@ function initPlater() {
|
|||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
orbit = new THREE.OrbitControls(camera, renderer.domElement);
|
orbit = new THREE.OrbitControls(camera, renderer.domElement);
|
||||||
orbit.enableDamping = true;
|
orbit.enableDamping = false;
|
||||||
|
orbit.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
|
||||||
orbit.target.set(0, 0, 0);
|
orbit.target.set(0, 0, 0);
|
||||||
|
|
||||||
transformProxy = new THREE.Object3D();
|
transformProxy = new THREE.Object3D();
|
||||||
@@ -228,6 +264,11 @@ function initPlater() {
|
|||||||
|
|
||||||
transformControl = new THREE.TransformControls(camera, renderer.domElement);
|
transformControl = new THREE.TransformControls(camera, renderer.domElement);
|
||||||
transformControl.setSpace('world');
|
transformControl.setSpace('world');
|
||||||
|
transformControl.addEventListener('change', function () {
|
||||||
|
if (transformControl.getMode() === 'scale' && !document.getElementById('scale-panel').classList.contains('d-none')) {
|
||||||
|
updateScalePanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
transformControl.addEventListener('dragging-changed', function (event) {
|
transformControl.addEventListener('dragging-changed', function (event) {
|
||||||
orbit.enabled = !event.value;
|
orbit.enabled = !event.value;
|
||||||
if (!event.value && activeModel) {
|
if (!event.value && activeModel) {
|
||||||
@@ -264,6 +305,13 @@ function setTransformMode(mode) {
|
|||||||
document.getElementById('btn-rotate').className = mode === 'rotate' ? 'btn btn-primary btn-sm rounded' : 'btn btn-outline-secondary btn-sm rounded';
|
document.getElementById('btn-rotate').className = mode === 'rotate' ? 'btn btn-primary btn-sm rounded' : 'btn btn-outline-secondary btn-sm rounded';
|
||||||
document.getElementById('btn-scale').className = mode === 'scale' ? 'btn btn-primary btn-sm rounded' : 'btn btn-outline-secondary btn-sm rounded';
|
document.getElementById('btn-scale').className = mode === 'scale' ? 'btn btn-primary btn-sm rounded' : 'btn btn-outline-secondary btn-sm rounded';
|
||||||
document.getElementById('btn-layflat').className = 'btn btn-outline-info btn-sm rounded';
|
document.getElementById('btn-layflat').className = 'btn btn-outline-info btn-sm rounded';
|
||||||
|
|
||||||
|
if (mode === 'scale' && activeModel) {
|
||||||
|
document.getElementById('scale-panel').classList.remove('d-none');
|
||||||
|
updateScalePanel();
|
||||||
|
} else {
|
||||||
|
document.getElementById('scale-panel').classList.add('d-none');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
layFlatMode = true;
|
layFlatMode = true;
|
||||||
document.getElementById('btn-translate').className = 'btn btn-outline-secondary btn-sm rounded';
|
document.getElementById('btn-translate').className = 'btn btn-outline-secondary btn-sm rounded';
|
||||||
@@ -271,9 +319,47 @@ function setTransformMode(mode) {
|
|||||||
document.getElementById('btn-scale').className = 'btn btn-outline-secondary btn-sm rounded';
|
document.getElementById('btn-scale').className = 'btn btn-outline-secondary btn-sm rounded';
|
||||||
document.getElementById('btn-layflat').className = 'btn btn-info btn-sm rounded text-white';
|
document.getElementById('btn-layflat').className = 'btn btn-info btn-sm rounded text-white';
|
||||||
transformControl.detach();
|
transformControl.detach();
|
||||||
|
document.getElementById('scale-panel').classList.add('d-none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateScalePanel() {
|
||||||
|
if (!activeModel) return;
|
||||||
|
const v = activeModel.getWorldScale(new THREE.Vector3());
|
||||||
|
document.getElementById('scale-x').value = v.x.toFixed(3);
|
||||||
|
document.getElementById('scale-y').value = v.y.toFixed(3);
|
||||||
|
document.getElementById('scale-z').value = v.z.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyScaleInput(axis) {
|
||||||
|
if (!activeModel) return;
|
||||||
|
let val = parseFloat(document.getElementById('scale-' + axis).value);
|
||||||
|
if (isNaN(val) || val <= 0.001) val = 1.0;
|
||||||
|
|
||||||
|
scene.attach(activeModel); // temporarily detach to operate purely on local=world scale
|
||||||
|
|
||||||
|
const isUniform = document.getElementById('scale-uniform').checked;
|
||||||
|
|
||||||
|
if (isUniform) {
|
||||||
|
// Find previous scale
|
||||||
|
const prev = activeModel.scale[axis];
|
||||||
|
const ratio = val / prev;
|
||||||
|
activeModel.scale.x *= ratio;
|
||||||
|
activeModel.scale.y *= ratio;
|
||||||
|
activeModel.scale.z *= ratio;
|
||||||
|
} else {
|
||||||
|
activeModel.scale[axis] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeModel.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
// re-attach proxy pivot logic without modifying the actual spatial scale
|
||||||
|
transformProxy.scale.set(1, 1, 1);
|
||||||
|
transformProxy.attach(activeModel);
|
||||||
|
|
||||||
|
updateScalePanel();
|
||||||
|
}
|
||||||
|
|
||||||
function removeActiveModel() {
|
function removeActiveModel() {
|
||||||
if (activeModel) {
|
if (activeModel) {
|
||||||
removeModel(activeModel);
|
removeModel(activeModel);
|
||||||
@@ -363,7 +449,12 @@ function selectModel(model) {
|
|||||||
transformProxy.scale.set(1, 1, 1);
|
transformProxy.scale.set(1, 1, 1);
|
||||||
transformProxy.attach(model);
|
transformProxy.attach(model);
|
||||||
transformControl.attach(transformProxy);
|
transformControl.attach(transformProxy);
|
||||||
|
if(transformControl.getMode() === 'scale') {
|
||||||
|
document.getElementById('scale-panel').classList.remove('d-none');
|
||||||
|
updateScalePanel();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
document.getElementById('scale-panel').classList.add('d-none');
|
||||||
transformControl.detach();
|
transformControl.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -500,7 +591,9 @@ function mergeAndSlice() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadedModels.length === 1) {
|
let isEdit = (loadedModels.length === 1 && String(loadedModels[0].userData.fileId) === String(initialAddId));
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
const singleModel = loadedModels[0];
|
const singleModel = loadedModels[0];
|
||||||
if (singleModel.userData.status === 'sliced') {
|
if (singleModel.userData.status === 'sliced') {
|
||||||
if (!confirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}")) {
|
if (!confirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}")) {
|
||||||
@@ -543,21 +636,21 @@ function mergeAndSlice() {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ pieces: pieces, quality: quality, infill: infill, support: support, support_pattern: supportPattern })
|
body: JSON.stringify({ pieces: pieces, quality: quality, infill: infill, support: support, support_pattern: supportPattern, is_edit: isEdit })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if(data.success) {
|
if(data.success) {
|
||||||
window.location.href = "{{ url_for('main.files') }}";
|
window.location.href = "{{ url_for('main.files') }}";
|
||||||
} else {
|
} else {
|
||||||
alert("Error: " + data.error);
|
alert("{{ _('Error:') }} " + data.error);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
icon.className = 'bi bi-gear-fill me-2';
|
icon.className = 'bi bi-gear-fill me-2';
|
||||||
text.innerText = '{{ _("Merge & Slice") }}';
|
text.innerText = '{{ _("Merge & Slice") }}';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
alert("Error: " + String(err));
|
alert("{{ _('Error:') }} " + String(err));
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
icon.className = 'bi bi-gear-fill me-2';
|
icon.className = 'bi bi-gear-fill me-2';
|
||||||
text.innerText = '{{ _("Merge & Slice") }}';
|
text.innerText = '{{ _("Merge & Slice") }}';
|
||||||
|
|||||||
@@ -52,5 +52,62 @@
|
|||||||
"Low Quality": "Low Quality",
|
"Low Quality": "Low Quality",
|
||||||
"Super Quality": "Super Quality",
|
"Super Quality": "Super Quality",
|
||||||
"Ultra Quality": "Ultra Quality",
|
"Ultra Quality": "Ultra Quality",
|
||||||
"Plater": "Plater"
|
"Plater": "Plater",
|
||||||
|
"Layer Progress:": "Layer Progress:",
|
||||||
|
"Loading and Parsing GCode Data...": "Loading and Parsing GCode Data...",
|
||||||
|
"Failed to load GCode preview.": "Failed to load GCode preview.",
|
||||||
|
"Outer Wall": "Outer Wall",
|
||||||
|
"Inner Wall": "Inner Wall",
|
||||||
|
"Infill": "Infill",
|
||||||
|
"Skin/TopBottom": "Skin/TopBottom",
|
||||||
|
"Travel (Move)": "Travel (Move)",
|
||||||
|
"Skirt": "Skirt",
|
||||||
|
"Support Interface": "Support Interface",
|
||||||
|
"Back": "Back",
|
||||||
|
"Layer": "Layer",
|
||||||
|
"Plater / Build Plate": "Plater / Build Plate",
|
||||||
|
"Translate (W)": "Translate (W)",
|
||||||
|
"Rotate (E)": "Rotate (E)",
|
||||||
|
"Scale (R)": "Scale (R)",
|
||||||
|
"Scale": "Scale",
|
||||||
|
"Uniform Scale": "Uniform Scale",
|
||||||
|
"Lay Flat": "Lay Flat",
|
||||||
|
"Remove Selected (Del)": "Remove Selected (Del)",
|
||||||
|
"Available Models": "Available Models",
|
||||||
|
"No STL models uploaded yet. Go upload some first.": "No STL models uploaded yet. Go upload some first.",
|
||||||
|
"Support Type": "Support Type",
|
||||||
|
"Tree": "Tree",
|
||||||
|
"Lines": "Lines",
|
||||||
|
"Grid": "Grid",
|
||||||
|
"Triangles": "Triangles",
|
||||||
|
"Concentric": "Concentric",
|
||||||
|
"Zig Zag": "Zig Zag",
|
||||||
|
"Cross": "Cross",
|
||||||
|
"Gyroid": "Gyroid",
|
||||||
|
"Honeycomb": "Honeycomb",
|
||||||
|
"Octagon": "Octagon",
|
||||||
|
"Clear Board": "Clear Board",
|
||||||
|
"Merge & Slice": "Merge & Slice",
|
||||||
|
"Error loading STL model file.": "Error loading STL model file.",
|
||||||
|
"Please add at least one model to the build plate.": "Please add at least one model to the build plate.",
|
||||||
|
"One or more models are outside the print area. Please adjust them before slicing.": "One or more models are outside the print area. Please adjust them before slicing.",
|
||||||
|
"Error:": "Error:",
|
||||||
|
"ID": "ID",
|
||||||
|
"Username": "Username",
|
||||||
|
"Role": "Role",
|
||||||
|
"Created At": "Created At",
|
||||||
|
"Admin": "Admin",
|
||||||
|
"User": "User",
|
||||||
|
"WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?": "WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?",
|
||||||
|
"CuraEngine Configurations": "CuraEngine Configurations",
|
||||||
|
"Plater Origin Offset X (mm)": "Plater Origin Offset X (mm)",
|
||||||
|
"Adjust the X-axis compilation offset for combined files on the build plate.": "Adjust the X-axis compilation offset for combined files on the build plate.",
|
||||||
|
"Plater Origin Offset Y (mm)": "Plater Origin Offset Y (mm)",
|
||||||
|
"Adjust the Y-axis compilation offset for combined files on the build plate.": "Adjust the Y-axis compilation offset for combined files on the build plate.",
|
||||||
|
"Default Plater Settings": "Default Plater Settings",
|
||||||
|
"Default Infill Density (%)": "Default Infill Density (%)",
|
||||||
|
"Default Support": "Default Support",
|
||||||
|
"Default Support Type": "Default Support Type",
|
||||||
|
"Default Quality Profile": "Default Quality Profile",
|
||||||
|
"Save Settings": "Save Settings"
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
"Original Name": "原始名称",
|
"Original Name": "原始名称",
|
||||||
"Status": "状态",
|
"Status": "状态",
|
||||||
"Actions": "操作",
|
"Actions": "操作",
|
||||||
"Uploaded": "已上传",
|
"Waiting": "等待中",
|
||||||
"Waiting": "等待",
|
"Merging": "合并中",
|
||||||
"Waiting in queue for slicing": "在队列中排队等待切片",
|
"Waiting in queue for slicing": "在队列中排队等待切片",
|
||||||
"Slicing": "切片中",
|
"Slicing": "切片中",
|
||||||
"Sliced": "已切片",
|
"Sliced": "已切片",
|
||||||
@@ -43,8 +43,7 @@
|
|||||||
"Upload Complete!": "上传完成!",
|
"Upload Complete!": "上传完成!",
|
||||||
"Upload error.": "上传出错。",
|
"Upload error.": "上传出错。",
|
||||||
"Upload failed.": "上传失败。",
|
"Upload failed.": "上传失败。",
|
||||||
"Please upload a valid .stl file!": "请上传有效的 .stl 文件!"
|
"Please upload a valid .stl file!": "请上传有效的 .stl 文件!",
|
||||||
,
|
|
||||||
"Slicing queued!": "切片已排队!",
|
"Slicing queued!": "切片已排队!",
|
||||||
"Draft Quality": "草稿质量",
|
"Draft Quality": "草稿质量",
|
||||||
"Standard Quality": "标准质量",
|
"Standard Quality": "标准质量",
|
||||||
@@ -53,5 +52,68 @@
|
|||||||
"Low Quality": "低质量",
|
"Low Quality": "低质量",
|
||||||
"Super Quality": "超高质量",
|
"Super Quality": "超高质量",
|
||||||
"Ultra Quality": "极高质量",
|
"Ultra Quality": "极高质量",
|
||||||
"Plater": "构建板"
|
"Plater": "构建板",
|
||||||
|
"Layer Progress:": "单层打印进度:",
|
||||||
|
"Loading and Parsing GCode Data...": "正在加载和解析 GCode 数据...",
|
||||||
|
"Failed to load GCode preview.": "加载 GCode 预览失败。",
|
||||||
|
"Outer Wall": "外墙",
|
||||||
|
"Inner Wall": "内墙",
|
||||||
|
"Infill": "填充",
|
||||||
|
"Skin/TopBottom": "顶层/底层",
|
||||||
|
"Travel (Move)": "空驶",
|
||||||
|
"Skirt": " 裙边",
|
||||||
|
"Support Interface": "支撑界面",
|
||||||
|
"Back": "返回",
|
||||||
|
"Layer": "层数",
|
||||||
|
"Plater / Build Plate": "构建板",
|
||||||
|
"Translate (W)": "平移 (W)",
|
||||||
|
"Rotate (E)": "旋转 (E)",
|
||||||
|
"Scale (R)": "缩放 (R)",
|
||||||
|
"Scale": "缩放",
|
||||||
|
"Uniform Scale": "均匀缩放",
|
||||||
|
"Lay Flat": "平放",
|
||||||
|
"Remove Selected (Del)": "删除选中 (Del)",
|
||||||
|
"Available Models": "可用模型",
|
||||||
|
"No STL models uploaded yet. Go upload some first.": "还没有上传 STL 模型。请先上传。",
|
||||||
|
"Other Settings": "其它设置",
|
||||||
|
"Infill Density": "填充密度",
|
||||||
|
"Support": "支撑",
|
||||||
|
"None": "无",
|
||||||
|
"Touching Buildplate": "仅接触构建板",
|
||||||
|
"Everywhere": "无处不在",
|
||||||
|
"Support Type": "支撑类型",
|
||||||
|
"Tree": "树状",
|
||||||
|
"Lines": "直线",
|
||||||
|
"Grid": "网格",
|
||||||
|
"Triangles": "三角形",
|
||||||
|
"Concentric": "同心",
|
||||||
|
"Zig Zag": "之字形",
|
||||||
|
"Cross": "交叉",
|
||||||
|
"Gyroid": "螺旋",
|
||||||
|
"Honeycomb": "蜂窝",
|
||||||
|
"Octagon": "八边形",
|
||||||
|
"Clear Board": "清空画板",
|
||||||
|
"Merge & Slice": "合并并切片",
|
||||||
|
"Error loading STL model file.": "加载 STL 模型文件出错。",
|
||||||
|
"Please add at least one model to the build plate.": "请在构建板上至少放置一个模型。",
|
||||||
|
"One or more models are outside the print area. Please adjust them before slicing.": "有一个或多个模型超出了打印范围。切片前请调整它们的位置。",
|
||||||
|
"Error:": "错误:",
|
||||||
|
"ID": "ID",
|
||||||
|
"Username": "用户名",
|
||||||
|
"Role": "角色",
|
||||||
|
"Created At": "创建时间",
|
||||||
|
"Admin": "管理员",
|
||||||
|
"User": "普通用户",
|
||||||
|
"WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?": "警告:确定要永久删除该用户以及TA上传的所有文件和切片吗?",
|
||||||
|
"CuraEngine Configurations": "CuraEngine 配置",
|
||||||
|
"Plater Origin Offset X (mm)": "构建板原点偏移 X (mm)",
|
||||||
|
"Adjust the X-axis compilation offset for combined files on the build plate.": "调整多文件在构建板合并切片时的X坐标偏移。",
|
||||||
|
"Plater Origin Offset Y (mm)": "构建板原点偏移 Y (mm)",
|
||||||
|
"Adjust the Y-axis compilation offset for combined files on the build plate.": "调整多文件在构建板合并切片时的Y坐标偏移。",
|
||||||
|
"Default Plater Settings": "默认构建板设置",
|
||||||
|
"Default Infill Density (%)": "默认填充密度 (%)",
|
||||||
|
"Default Support": "默认支撑类型",
|
||||||
|
"Default Support Type": "默认支撑图案",
|
||||||
|
"Default Quality Profile": "默认质量配置",
|
||||||
|
"Save Settings": "保存设置"
|
||||||
}
|
}
|
||||||
1
assets/js/gcode-preview.min.js
vendored
Normal file
1
assets/js/gcode-preview.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
huey_queue.db
BIN
huey_queue.db
Binary file not shown.
Binary file not shown.
13
print_config/materials/generic_petg.inst.cfg
Normal file
13
print_config/materials/generic_petg.inst.cfg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[values]
|
||||||
|
material_print_temperature = 240
|
||||||
|
material_print_temperature_layer_0 = 240
|
||||||
|
material_initial_print_temperature = 240
|
||||||
|
material_final_print_temperature = 240
|
||||||
|
material_extrusion_cool_down_speed = 0.7
|
||||||
|
material_bed_temperature = 80
|
||||||
|
material_bed_temperature_layer_0 = 80
|
||||||
|
material_shrinkage_percentage = 100
|
||||||
|
material_flow = 100
|
||||||
|
cool_fan_enabled = true
|
||||||
|
cool_fan_speed_min = 35
|
||||||
|
cool_fan_speed_max = 100
|
||||||
13
print_config/materials/generic_pla.inst.cfg
Normal file
13
print_config/materials/generic_pla.inst.cfg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[values]
|
||||||
|
material_print_temperature = 200
|
||||||
|
material_print_temperature_layer_0 = 205
|
||||||
|
material_initial_print_temperature = 205
|
||||||
|
material_final_print_temperature = 200
|
||||||
|
material_extrusion_cool_down_speed = 0.7
|
||||||
|
material_bed_temperature = 60
|
||||||
|
material_bed_temperature_layer_0 = 60
|
||||||
|
material_shrinkage_percentage = 100
|
||||||
|
material_flow = 100
|
||||||
|
cool_fan_enabled = true
|
||||||
|
cool_fan_speed_min = 100
|
||||||
|
cool_fan_speed_max = 100
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
"retraction_amount": { "value": 0.8 },
|
"retraction_amount": { "value": 0.8 },
|
||||||
"retraction_speed": { "default_value": 40 },
|
"retraction_speed": { "default_value": 40 },
|
||||||
"speed_layer_0": { "value": 30 },
|
"speed_layer_0": { "value": 30 },
|
||||||
"speed_print": { "value": 180 }
|
"speed_print": { "value": 180 },
|
||||||
|
"meshfix_union_all": { "value": true },
|
||||||
|
"meshfix_remove_duplicate_faces": { "value": true },
|
||||||
|
"meshfix_remove_intersecting_faces": { "value": true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4435,7 +4435,9 @@
|
|||||||
"concentric": "Concentric",
|
"concentric": "Concentric",
|
||||||
"zigzag": "Zig Zag",
|
"zigzag": "Zig Zag",
|
||||||
"cross": "Cross",
|
"cross": "Cross",
|
||||||
"gyroid": "Gyroid"
|
"gyroid": "Gyroid",
|
||||||
|
"honeycomb": "Honeycomb",
|
||||||
|
"octagon": "Octagon"
|
||||||
},
|
},
|
||||||
"default_value": "zigzag",
|
"default_value": "zigzag",
|
||||||
"enabled": "support_enable or support_meshes_present",
|
"enabled": "support_enable or support_meshes_present",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[general]
|
||||||
|
version = 4
|
||||||
|
name = Ultra Quality
|
||||||
|
definition = creality_base
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
setting_version = 19
|
||||||
|
type = quality
|
||||||
|
quality_type = ultra
|
||||||
|
material = generic_petg
|
||||||
|
variant = 0.4mm Nozzle
|
||||||
|
|
||||||
|
[values]
|
||||||
|
speed_layer_0 = 15
|
||||||
|
wall_thickness = =line_width*4
|
||||||
|
#retraction_extra_prime_amount = 0.5
|
||||||
11
print_config/supports/normal.inst.cfg
Normal file
11
print_config/supports/normal.inst.cfg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[values]
|
||||||
|
support_angle = 50.0
|
||||||
|
support_wall_count = 1
|
||||||
|
support_infill_rate = 15.0
|
||||||
|
support_line_distance = 2.5
|
||||||
|
support_brim_enable = true
|
||||||
|
support_brim_width = 4.0
|
||||||
|
support_z_distance = 0.2
|
||||||
|
support_xy_distance = 0.7
|
||||||
|
support_top_distance = 0.2
|
||||||
|
support_bottom_distance = 0.2
|
||||||
22
print_config/supports/tree.inst.cfg
Normal file
22
print_config/supports/tree.inst.cfg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[values]
|
||||||
|
support_angle = 45
|
||||||
|
support_infill_rate = 0
|
||||||
|
support_tree_angle = 45.0
|
||||||
|
support_tree_branch_diameter = 5.0
|
||||||
|
support_tree_max_diameter = 25.0
|
||||||
|
support_tree_branch_diameter_angle = 7.0
|
||||||
|
support_tree_angle_slow = 50.0
|
||||||
|
support_tree_max_diameter_increase_by_merges_when_support_to_model = 1.0
|
||||||
|
support_tree_min_height_to_model = 3.0
|
||||||
|
support_tree_bp_diameter = 7.5
|
||||||
|
support_tree_top_rate = 15.0
|
||||||
|
support_tree_tip_diameter = 0.4
|
||||||
|
support_tree_limit_branch_reach = false
|
||||||
|
support_tree_branch_reach_limit = 30.0
|
||||||
|
support_tree_rest_preference = buildplate
|
||||||
|
support_brim_enable = true
|
||||||
|
support_brim_width = 4.0
|
||||||
|
support_z_distance = 0.2
|
||||||
|
support_xy_distance = 1.0
|
||||||
|
support_top_distance = 0.2
|
||||||
|
support_bottom_distance = 0.2
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
[general]
|
[general]
|
||||||
|
definition = creality_ender3v3se
|
||||||
name = 0.2mm Nozzle
|
name = 0.2mm Nozzle
|
||||||
version = 4
|
version = 4
|
||||||
definition = creality_ender3
|
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
setting_version = 19
|
|
||||||
type = variant
|
|
||||||
hardware_type = nozzle
|
hardware_type = nozzle
|
||||||
|
setting_version = 26
|
||||||
|
type = variant
|
||||||
|
|
||||||
[values]
|
[values]
|
||||||
machine_nozzle_size = 0.2
|
machine_nozzle_size = 0.2
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user