能用prusa切片和预览了,添加了缺失的翻译

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-24 01:51:08 +08:00
parent 22a6493e24
commit 366372da6e
15 changed files with 394 additions and 38 deletions

View File

@@ -218,5 +218,12 @@
"Add User": "Benutzer hinzufügen", "Add User": "Benutzer hinzufügen",
"Password": "Passwort", "Password": "Passwort",
"Is Admin": "Ist Administrator", "Is Admin": "Ist Administrator",
"Create User": "Benutzer erstellen" "Create User": "Benutzer erstellen",
"Build Plate Model Path (.stl)": "Pfad zum Build-Platte-Modell (.stl)",
"Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.": "Absoluter Pfad zum benutzerdefinierten Build-Platte-STL-Modell, das im Plater angezeigt werden soll. Leer lassen, um keines zu verwenden.",
"Default Material Profile": "Standard Materialprofil",
"Slicing Engine Configurations": "Slicer-Engine-Konfigurationen",
"Slicing Engine": "Slicer-Engine",
"Select the engine to be used globally. Ensure the selected engine is installed and accessible on the server.": "Wählen Sie die Engine aus, die global verwendet werden soll. Stellen Sie sicher, dass die ausgewählte Engine installiert und auf dem Server zugänglich ist.",
"Material Profile": "Materialprofil"
} }

View File

@@ -218,5 +218,12 @@
"Add User": "Add User", "Add User": "Add User",
"Password": "Password", "Password": "Password",
"Is Admin": "Is Admin", "Is Admin": "Is Admin",
"Create User": "Create User" "Create User": "Create User",
"Build Plate Model Path (.stl)": "Build Plate Model Path (.stl)",
"Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.": "Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.",
"Default Material Profile": "Default Material Profile",
"Slicing Engine Configurations": "Slicing Engine Configurations",
"Slicing Engine": "Slicing Engine",
"Select the engine to be used globally. Ensure the selected engine is installed and accessible on the server.": "Select the engine to be used globally. Ensure the selected engine is installed and accessible on the server.",
"Material Profile": "Material Profile"
} }

View File

@@ -218,5 +218,12 @@
"Add User": "添加用户", "Add User": "添加用户",
"Password": "密码", "Password": "密码",
"Is Admin": "设为管理员", "Is Admin": "设为管理员",
"Create User": "创建用户" "Create User": "创建用户",
"Build Plate Model Path (.stl)": "构建板模型路径 (.stl)",
"Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.": "保存自定义构建板 STL 模型的绝对路径,以在 plater 中显示。留空以使用默认值。",
"Default Material Profile": "默认材料配置",
"Slicing Engine Configurations": "切片引擎配置",
"Slicing Engine": "切片引擎",
"Select the engine to be used globally. Ensure the selected engine is installed and accessible on the server.": "选择要全局使用的引擎。确保所选引擎已安装且在服务器上可访问。",
"Material Profile": "材料配置"
} }

View File

@@ -46,6 +46,7 @@ def settings():
default_support = request.form.get('default_support', 'false') default_support = request.form.get('default_support', 'false')
default_support_pattern = request.form.get('default_support_pattern', 'tree') default_support_pattern = request.form.get('default_support_pattern', 'tree')
default_quality = request.form.get('default_quality', 'base_global_standard.inst.cfg') default_quality = request.form.get('default_quality', 'base_global_standard.inst.cfg')
default_material = request.form.get('default_material', '')
gcode_upload_folder = request.form.get('gcode_upload_folder', '').strip() gcode_upload_folder = request.form.get('gcode_upload_folder', '').strip()
slicer_engine = request.form.get('slicer_engine', 'cura') slicer_engine = request.form.get('slicer_engine', 'cura')
build_plate_model_path = request.form.get('build_plate_model_path', '').strip() build_plate_model_path = request.form.get('build_plate_model_path', '').strip()
@@ -59,6 +60,7 @@ def settings():
('default_support', default_support), ('default_support', default_support),
('default_support_pattern', default_support_pattern), ('default_support_pattern', default_support_pattern),
('default_quality', default_quality), ('default_quality', default_quality),
('default_material', default_material),
('gcode_upload_folder', gcode_upload_folder), ('gcode_upload_folder', gcode_upload_folder),
('slicer_engine', slicer_engine), ('slicer_engine', slicer_engine),
('build_plate_model_path', build_plate_model_path), ('build_plate_model_path', build_plate_model_path),

View File

@@ -355,10 +355,11 @@ def plater():
default_support = configs.get('default_support', 'false') default_support = configs.get('default_support', 'false')
default_support_pattern = configs.get('default_support_pattern', 'tree') default_support_pattern = configs.get('default_support_pattern', 'tree')
default_quality = configs.get('default_quality', 'base_global_standard.inst.cfg') default_quality = configs.get('default_quality', 'base_global_standard.inst.cfg')
default_material = configs.get('default_material', '')
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('slice/plater.html', w=w, h=h, hd=hd, last_quality=default_quality, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern, quota_exceeded=quota_exceeded, configs=configs) return render_template('slice/plater.html', w=w, h=h, hd=hd, last_quality=default_quality, last_material=default_material, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern, quota_exceeded=quota_exceeded, configs=configs)
@main_bp.route('/file/<int:file_id>') @main_bp.route('/file/<int:file_id>')
@login_required @login_required
@@ -390,6 +391,7 @@ def merge_and_slice():
data = request.json data = request.json
pieces = data.get('pieces', []) pieces = data.get('pieces', [])
quality = data.get('quality', 'base_global_standard.inst.cfg') quality = data.get('quality', 'base_global_standard.inst.cfg')
material = data.get('material', '')
infill_density = data.get('infill', '20') infill_density = data.get('infill', '20')
support_enable = data.get('support', 'false') support_enable = data.get('support', 'false')
support_pattern = data.get('support_pattern', 'lines') support_pattern = data.get('support_pattern', 'lines')
@@ -427,6 +429,7 @@ def merge_and_slice():
"matrix": p['raw_matrix'], "matrix": p['raw_matrix'],
"settings": { "settings": {
"quality": quality, "quality": quality,
"material": material,
"infill": infill_density, "infill": infill_density,
"support": support_enable, "support": support_enable,
"support_pattern": support_pattern "support_pattern": support_pattern
@@ -450,6 +453,7 @@ def merge_and_slice():
"parts": [], "parts": [],
"settings": { "settings": {
"quality": quality, "quality": quality,
"material": material,
"infill": infill_density, "infill": infill_density,
"support": support_enable, "support": support_enable,
"support_pattern": support_pattern "support_pattern": support_pattern
@@ -471,6 +475,7 @@ def merge_and_slice():
"matrix": pieces[0].get('raw_matrix', pieces[0]['matrix']), "matrix": pieces[0].get('raw_matrix', pieces[0]['matrix']),
"settings": { "settings": {
"quality": quality, "quality": quality,
"material": material,
"infill": infill_density, "infill": infill_density,
"support": support_enable, "support": support_enable,
"support_pattern": support_pattern "support_pattern": support_pattern
@@ -482,7 +487,7 @@ def merge_and_slice():
temp_filename = f"temp_edit_{uuid.uuid4().hex}.stl" temp_filename = f"temp_edit_{uuid.uuid4().hex}.stl"
temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename) temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename)
merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=True) merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, material, infill_density, support_enable, support_pattern, delete_stl=True)
elif len(inputs) == 1 and is_edit: elif len(inputs) == 1 and is_edit:
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)
@@ -495,7 +500,7 @@ def merge_and_slice():
temp_filename = f"temp_{uuid.uuid4().hex}.stl" temp_filename = f"temp_{uuid.uuid4().hex}.stl"
temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename) temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename)
merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=True) merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, material, infill_density, support_enable, support_pattern, delete_stl=True)
else: else:
# Multiple models, create a new "Merged Slice" PrintFile entry to keep track of combination # Multiple models, create a new "Merged Slice" PrintFile entry to keep track of combination
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
@@ -508,6 +513,7 @@ def merge_and_slice():
"parts": [], "parts": [],
"settings": { "settings": {
"quality": quality, "quality": quality,
"material": material,
"infill": infill_density, "infill": infill_density,
"support": support_enable, "support": support_enable,
"support_pattern": support_pattern "support_pattern": support_pattern
@@ -534,7 +540,7 @@ def merge_and_slice():
db.session.add(print_file) db.session.add(print_file)
db.session.commit() db.session.commit()
merge_and_slice_task(print_file.id, inputs, merged_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=False) merge_and_slice_task(print_file.id, inputs, merged_filepath, quality, material, infill_density, support_enable, support_pattern, delete_stl=False)
return jsonify({'success': True, 'message': 'Plater slice queued!'}) return jsonify({'success': True, 'message': 'Plater slice queued!'})
@@ -553,5 +559,6 @@ def engine_options(engine_name):
from app.utils.slice_engines import get_slicer_engine from app.utils.slice_engines import get_slicer_engine
engine = get_slicer_engine(engine_name) engine = get_slicer_engine(engine_name)
presets = engine.get_quality_presets(current_app) presets = engine.get_quality_presets(current_app)
patterns = engine.get_support_patterns() patterns = engine.get_support_patterns(current_app)
return jsonify({'presets': presets, 'support_patterns': patterns}) materials = engine.get_materials(current_app) if hasattr(engine, 'get_materials') else []
return jsonify({'presets': presets, 'support_patterns': patterns, 'materials': materials})

View File

@@ -12,25 +12,25 @@
<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"><i class="bi bi-info-circle me-1"></i>{{ _('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"><i class="bi bi-info-circle me-1"></i>{{ _('Adjust the Y-axis compilation offset for combined files on the build plate.') }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="proxy_skip_size_mb" class="form-label">{{ _('Proxy Skip Size (MB)') }}</label> <label for="proxy_skip_size_mb" class="form-label">{{ _('Proxy Skip Size (MB)') }}</label>
<input type="number" class="form-control" name="proxy_skip_size_mb" id="proxy_skip_size_mb" value="{{ configs.get('proxy_skip_size_mb', '5.0') }}" step="0.1" min="0"> <input type="number" class="form-control" name="proxy_skip_size_mb" id="proxy_skip_size_mb" value="{{ configs.get('proxy_skip_size_mb', '5.0') }}" step="0.1" min="0">
<div class="form-text">{{ _('Files smaller than this will not generate a simplified proxy.') }}</div> <div class="form-text"><i class="bi bi-info-circle me-1"></i>{{ _('Files smaller than this will not generate a simplified proxy.') }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="gcode_upload_folder" class="form-label"><i class="bi bi-folder2-open me-2"></i>{{ _('Custom GCode Output Folder') }}</label> <label for="gcode_upload_folder" class="form-label"><i class="bi bi-folder2-open me-2"></i>{{ _('Custom GCode Output Folder') }}</label>
<input type="text" class="form-control" name="gcode_upload_folder" id="gcode_upload_folder" value="{{ configs.get('gcode_upload_folder', '') }}"> <input type="text" class="form-control" name="gcode_upload_folder" id="gcode_upload_folder" value="{{ configs.get('gcode_upload_folder', '') }}">
<div class="form-text">{{ _('Absolute path to save locally sliced GCode files (e.g. OctoPrint uploads folder like "/home/pi/.octoprint/uploads"). Leave empty to use system default.') }}</div> <div class="form-text"><i class="bi bi-info-circle me-1"></i>{{ _('Absolute path to save locally sliced GCode files (e.g. OctoPrint uploads folder like "/home/pi/.octoprint/uploads"). Leave empty to use system default.') }}</div>
</div> </div>
<h5 class="card-title text-primary border-bottom pb-2 mt-4 mb-3"><i class="bi bi-grid-3x3 me-2"></i>{{ _('Default Plater Settings') }}</h5> <h5 class="card-title text-primary border-bottom pb-2 mt-4 mb-3"><i class="bi bi-grid-3x3 me-2"></i>{{ _('Default Plater Settings') }}</h5>
@@ -38,7 +38,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="build_plate_model_path" class="form-label">{{ _('Build Plate Model Path (.stl)') }}</label> <label for="build_plate_model_path" class="form-label">{{ _('Build Plate Model Path (.stl)') }}</label>
<input type="text" class="form-control" name="build_plate_model_path" id="build_plate_model_path" value="{{ configs.get('build_plate_model_path', '') }}"> <input type="text" class="form-control" name="build_plate_model_path" id="build_plate_model_path" value="{{ configs.get('build_plate_model_path', '') }}">
<div class="form-text">{{ _('Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.') }}</div> <div class="form-text"><i class="bi bi-info-circle me-1"></i>{{ _('Absolute path to a custom build plate STL model to show in the plater. Leave empty to use none.') }}</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -62,13 +62,20 @@
</select> </select>
</div> </div>
<div class="mb-4"> <div class="mb-3">
<label for="default_quality" class="form-label">{{ _('Default Quality Profile') }}</label> <label for="default_quality" class="form-label">{{ _('Default Quality Profile') }}</label>
<select class="form-select" name="default_quality" id="default_quality" data-selected="{{ configs.get('default_quality', 'base_global_standard.inst.cfg') }}"> <select class="form-select" name="default_quality" id="default_quality" data-selected="{{ configs.get('default_quality', 'base_global_standard.inst.cfg') }}">
<!-- Loaded via JS --> <!-- Loaded via JS -->
</select> </select>
</div> </div>
<div class="mb-4">
<label for="default_material" class="form-label">{{ _('Default Material Profile') }}</label>
<select class="form-select" name="default_material" id="default_material" data-selected="{{ configs.get('default_material', '') }}">
<!-- Loaded via JS -->
</select>
</div>
<h5 class="card-title text-primary border-bottom pb-2 mt-4 mb-3"><i class="bi bi-cpu me-2"></i>{{ _('Slicing Engine Configurations') }}</h5> <h5 class="card-title text-primary border-bottom pb-2 mt-4 mb-3"><i class="bi bi-cpu me-2"></i>{{ _('Slicing Engine Configurations') }}</h5>
<div class="mb-3"> <div class="mb-3">
<label for="slicer_engine" class="form-label">{{ _('Slicing Engine') }}</label> <label for="slicer_engine" class="form-label">{{ _('Slicing Engine') }}</label>
@@ -151,6 +158,7 @@ function submitSettings(event) {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const engineSelect = document.getElementById('slicer_engine'); const engineSelect = document.getElementById('slicer_engine');
const qualitySelect = document.getElementById('default_quality'); const qualitySelect = document.getElementById('default_quality');
const materialSelect = document.getElementById('default_material');
const patternSelect = document.getElementById('default_support_pattern'); const patternSelect = document.getElementById('default_support_pattern');
function updateOptions(engine) { function updateOptions(engine) {
@@ -166,6 +174,21 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
if(selQ) qualitySelect.value = selQ; if(selQ) qualitySelect.value = selQ;
const selM = materialSelect.getAttribute('data-selected');
materialSelect.innerHTML = '';
// Add an empty option for material (optional fallback)
const emptyOpt = document.createElement('option');
emptyOpt.value = ''; emptyOpt.textContent = "{{ _('Auto / Default') }}";
materialSelect.appendChild(emptyOpt);
if(data.materials) {
data.materials.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id; opt.textContent = p.name;
materialSelect.appendChild(opt);
});
}
if(selM) materialSelect.value = selM;
const selP = patternSelect.getAttribute('data-selected'); const selP = patternSelect.getAttribute('data-selected');
patternSelect.innerHTML = ''; patternSelect.innerHTML = '';
data.support_patterns.forEach(p => { data.support_patterns.forEach(p => {

View File

@@ -65,6 +65,7 @@
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="langDropdown"> <ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="langDropdown">
<li><a class="dropdown-item {% if request.cookies.get('lang') == 'en' %}active{% endif %}" href="{{ url_for('main.set_language', lang='en') }}"><i class="bi bi-translate me-2"></i>English</a></li> <li><a class="dropdown-item {% if request.cookies.get('lang') == 'en' %}active{% endif %}" href="{{ url_for('main.set_language', lang='en') }}"><i class="bi bi-translate me-2"></i>English</a></li>
<li><a class="dropdown-item {% if request.cookies.get('lang') == 'zh-cn' %}active{% endif %}" href="{{ url_for('main.set_language', lang='zh-cn') }}"><i class="bi bi-translate me-2"></i>中文 (简体)</a></li> <li><a class="dropdown-item {% if request.cookies.get('lang') == 'zh-cn' %}active{% endif %}" href="{{ url_for('main.set_language', lang='zh-cn') }}"><i class="bi bi-translate me-2"></i>中文 (简体)</a></li>
<li><a class="dropdown-item {% if request.cookies.get('lang') == 'de' %}active{% endif %}" href="{{ url_for('main.set_language', lang='de') }}"><i class="bi bi-translate me-2"></i>Deutsch</a></li>
</ul> </ul>
</div> </div>

View File

@@ -66,9 +66,26 @@ document.addEventListener('DOMContentLoaded', async function() {
'SKIRT': new THREE.Color(0x00ffff), 'SKIRT': new THREE.Color(0x00ffff),
'SUPPORT-INTERFACE': new THREE.Color(0x2b6b2b), 'SUPPORT-INTERFACE': new THREE.Color(0x2b6b2b),
'TRAVEL': new THREE.Color(0x405060), 'TRAVEL': new THREE.Color(0x405060),
'DEFAULT': new THREE.Color(0xaaaaaa), 'DEFAULT': new THREE.Color(0xaaaaaa)
}; };
// Additional aliases mapped to basic COLORS
const COLOR_ALIASES = {
'External perimeter': COLORS['WALL-OUTER'],
'Overhang perimeter': COLORS['WALL-OUTER'],
'Perimeter': COLORS['WALL-INNER'],
'Internal infill': COLORS['FILL'],
'Solid infill': COLORS['FILL'],
'Top solid infill': COLORS['FILL'],
'Bridge infill': COLORS['FILL'],
'Support material': COLORS['SUPPORT'],
'Skirt/Brim': COLORS['SKIRT'],
'Support material interface': COLORS['SUPPORT-INTERFACE']
};
// Merge aliases into COLORS
Object.assign(COLORS, COLOR_ALIASES);
// Inject printer machine dimensions via Jinja // Inject printer machine dimensions via Jinja
const bedWidth = {{ machine_width | default(220) }}; const bedWidth = {{ machine_width | default(220) }};
const bedDepth = {{ machine_depth | default(220) }}; const bedDepth = {{ machine_depth | default(220) }};
@@ -83,6 +100,23 @@ document.addEventListener('DOMContentLoaded', async function() {
'SKIRT': 7, 'SUPPORT-INTERFACE': 8 'SKIRT': 7, 'SUPPORT-INTERFACE': 8
}; };
// Aliases for TYPE_INDEX
const TYPE_INDEX_ALIASES = {
'External perimeter': 1,
'Overhang perimeter': 1,
'Perimeter': 2,
'Internal infill': 3,
'Solid infill': 3,
'Top solid infill': 3,
'Bridge infill': 3,
'Support material': 5,
'Skirt/Brim': 7,
'Support material interface': 8
};
// Merge aliases into TYPE_INDEX
Object.assign(TYPE_INDEX, TYPE_INDEX_ALIASES);
let layers = []; let layers = [];
let scene, camera, renderer, controls; let scene, camera, renderer, controls;
let group = new THREE.Group(); let group = new THREE.Group();
@@ -160,6 +194,21 @@ document.addEventListener('DOMContentLoaded', async function() {
'TRAVEL': 'uShowTravel' 'TRAVEL': 'uShowTravel'
}; };
const uniformMapAliases = {
'External perimeter': 'uShowOuter',
'Overhang perimeter': 'uShowOuter',
'Perimeter': 'uShowInner',
'Internal infill': 'uShowInfill',
'Solid infill': 'uShowInfill',
'Top solid infill': 'uShowInfill',
'Bridge infill': 'uShowInfill',
'Support material': 'uShowSupport',
'Skirt/Brim': 'uShowSkirt',
'Support material interface': 'uShowSupportInterface'
};
Object.assign(uniformMap, uniformMapAliases);
document.querySelectorAll('.legend-item').forEach(el => { document.querySelectorAll('.legend-item').forEach(el => {
el.addEventListener('click', function() { el.addEventListener('click', function() {
const t = this.dataset.type; const t = this.dataset.type;
@@ -292,16 +341,29 @@ document.addEventListener('DOMContentLoaded', async function() {
} }
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let chunk = lines[i].trim().toUpperCase(); let chunk = lines[i].trim();
if (!chunk) continue; if (!chunk) continue;
let upperChunk = chunk.toUpperCase();
if (chunk.startsWith(';LAYER:')) { if (upperChunk.startsWith(';LAYER:')) {
flushLayer(); flushLayer();
} else if (chunk.startsWith(';TYPE:')) { } else if (upperChunk.startsWith(';TYPE:')) {
currentTypeStr = chunk.substring(6).trim(); currentTypeStr = chunk.substring(6).trim();
} else if (chunk.startsWith('G0') || chunk.startsWith('G1')) { } else if (upperChunk.startsWith(';') && chunk.includes(' perimeter')) {
// Heuristics for Prusa/Slic3r specific comments like `; External perimeter`
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes(' infill')) {
// Heuristics for Prusa/Slic3r specific comments like `; Internal infill`
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes(' material')) {
// Support material
currentTypeStr = chunk.substring(1).trim();
} else if (upperChunk.startsWith(';') && chunk.includes('Skirt/Brim')) {
// Skirt/Brim
currentTypeStr = 'Skirt/Brim';
} else if (upperChunk.startsWith('G0') || upperChunk.startsWith('G1')) {
let next = { x: current.x, y: current.y, z: current.z, e: current.e }; let next = { x: current.x, y: current.y, z: current.z, e: current.e };
let parts = chunk.split(/\s+/); let parts = upperChunk.split(/\s+/);
let hasMove = false; let hasMove = false;
for (let p of parts) { for (let p of parts) {
@@ -314,11 +376,17 @@ document.addEventListener('DOMContentLoaded', async function() {
if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) { if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) {
let isExtrude = (next.e > current.e); let isExtrude = (next.e > current.e);
// Cura uses G0 for travel generally // Cura uses G0 for travel generally
if (chunk.startsWith('G0') && !chunk.includes('E')) isExtrude = false; if (upperChunk.startsWith('G0') && !upperChunk.includes('E')) isExtrude = false;
let activeType = isExtrude ? currentTypeStr : 'TRAVEL'; let activeType = isExtrude ? currentTypeStr : 'TRAVEL';
let col = COLORS[activeType] || COLORS['DEFAULT']; // Special case for default aliases like "; External perimeter" which we stored in currentTypeStr
let tIdx = TYPE_INDEX[activeType] !== undefined ? TYPE_INDEX[activeType] : TYPE_INDEX['DEFAULT']; let resolvedType = activeType;
if (isExtrude && Object.keys(COLOR_ALIASES).includes(activeType)) {
resolvedType = activeType;
}
let col = COLORS[resolvedType] || COLORS['DEFAULT'];
let tIdx = TYPE_INDEX[resolvedType] !== undefined ? TYPE_INDEX[resolvedType] : TYPE_INDEX['DEFAULT'];
if (isExtrude) { if (isExtrude) {
let dx = next.x - current.x; let dx = next.x - current.x;

View File

@@ -101,6 +101,20 @@
</div> </div>
</div> </div>
<div class="card shadow-sm flex-shrink-0 mb-3">
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseMaterial" aria-expanded="false">
<span><i class="bi bi-box me-2"></i>{{ _('Material Profile') }}</span>
<i class="bi bi-chevron-bar-contract"></i>
</div>
<div id="collapseMaterial" class="collapse" data-bs-parent="#platerSidebarAccordion">
<div class="card-body">
<div class="mb-3">
<select class="form-select bg-light" id="material" data-selected="{{ last_material }}"></select>
</div>
</div>
</div>
</div>
<div class="card shadow-sm flex-shrink-0 mb-3"> <div class="card shadow-sm flex-shrink-0 mb-3">
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseQuality" aria-expanded="false"> <div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseQuality" aria-expanded="false">
<span><i class="bi bi-gear-wide-connected me-2"></i>{{ _('Quality Profile') }}</span> <span><i class="bi bi-gear-wide-connected me-2"></i>{{ _('Quality Profile') }}</span>
@@ -679,6 +693,11 @@ function addModelToPlate(btnElement, fileId, url, name, status) {
qSelect.setAttribute('data-selected', data.settings.quality); qSelect.setAttribute('data-selected', data.settings.quality);
qSelect.value = data.settings.quality; qSelect.value = data.settings.quality;
} }
if (data.settings.material) {
const mSelect = document.getElementById('material');
mSelect.setAttribute('data-selected', data.settings.material);
mSelect.value = data.settings.material;
}
} }
} catch (e) {} } catch (e) {}
} }
@@ -882,6 +901,7 @@ function mergeAndSlice() {
}); });
const quality = document.getElementById('quality').value; const quality = document.getElementById('quality').value;
const material = document.getElementById('material').value;
const infill = document.getElementById('infill-density').value; const infill = document.getElementById('infill-density').value;
const support = document.getElementById('support-type').value; const support = document.getElementById('support-type').value;
const supportPattern = document.getElementById('support-pattern').value; const supportPattern = document.getElementById('support-pattern').value;
@@ -900,7 +920,7 @@ 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, is_edit: isEdit, target_file_id: targetFileId }) body: JSON.stringify({ pieces: pieces, quality: quality, material: material, infill: infill, support: support, support_pattern: supportPattern, is_edit: isEdit, target_file_id: targetFileId })
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@@ -959,6 +979,7 @@ document.addEventListener('DOMContentLoaded', () => {
const engine = "{{ configs.get('slicer_engine', 'cura') }}"; const engine = "{{ configs.get('slicer_engine', 'cura') }}";
const qualitySelect = document.getElementById('quality'); const qualitySelect = document.getElementById('quality');
const patternSelect = document.getElementById('support-pattern'); const patternSelect = document.getElementById('support-pattern');
const materialSelect = document.getElementById('material');
fetch(`/api/engine_options/${engine}`) fetch(`/api/engine_options/${engine}`)
.then(res => res.json()) .then(res => res.json())
@@ -972,6 +993,20 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
if(selQ) qualitySelect.value = selQ; if(selQ) qualitySelect.value = selQ;
const selM = materialSelect.getAttribute('data-selected');
materialSelect.innerHTML = '';
const emptyOpt = document.createElement('option');
emptyOpt.value = ''; emptyOpt.textContent = "{{ _('Auto / Default') }}";
materialSelect.appendChild(emptyOpt);
if(data.materials) {
data.materials.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id; opt.textContent = p.name;
materialSelect.appendChild(opt);
});
}
if(selM) materialSelect.value = selM;
const selP = patternSelect.getAttribute('data-selected'); const selP = patternSelect.getAttribute('data-selected');
patternSelect.innerHTML = ''; patternSelect.innerHTML = '';
data.support_patterns.forEach(p => { data.support_patterns.forEach(p => {

View File

@@ -68,12 +68,17 @@ class CuraEngine:
preset_path = os.path.join(presets_path, 'creality', 'presets', quality_preset) preset_path = os.path.join(presets_path, 'creality', 'presets', quality_preset)
if os.path.exists(preset_path): if os.path.exists(preset_path):
config.read(preset_path) config.read(preset_path)
material_type = config.get('metadata', 'material', fallback=None) material_type_from_preset = config.get('metadata', 'material', fallback=None)
variant_type = config.get('metadata', 'variant', fallback=None) variant_type = config.get('metadata', 'variant', fallback=None)
quality_type = config.get('metadata', 'quality_type', fallback=None) quality_type = config.get('metadata', 'quality_type', fallback=None)
# Use explicit material if provided, otherwise fallback to preset's material
material_type = kwargs.get('material_preset') or material_type_from_preset
if material_type: if material_type:
m_path = os.path.join(materials_path, f"{material_type}.inst.cfg") m_path = os.path.join(materials_path, f"{material_type}.inst.cfg")
if not os.path.exists(m_path) and kwargs.get('material_preset'):
m_path = os.path.join(materials_path, f"{kwargs.get('material_preset')}")
if os.path.exists(m_path): inst_files_list.append(m_path) if os.path.exists(m_path): inst_files_list.append(m_path)
if variant_type: if variant_type:
variant_d = variant_type.split("mm")[0] variant_d = variant_type.split("mm")[0]
@@ -200,7 +205,7 @@ class CuraEngine:
except: except:
return [] return []
def get_support_patterns(self): def get_support_patterns(self, app):
return [ return [
{'id': 'tree', 'name': 'Tree'}, {'id': 'tree', 'name': 'Tree'},
{'id': 'lines', 'name': 'Lines'}, {'id': 'lines', 'name': 'Lines'},
@@ -213,3 +218,16 @@ class CuraEngine:
{'id': 'honeycomb', 'name': 'Honeycomb'}, {'id': 'honeycomb', 'name': 'Honeycomb'},
{'id': 'octagon', 'name': 'Octagon'} {'id': 'octagon', 'name': 'Octagon'}
] ]
def get_materials(self, app):
try:
path = os.path.join(app.root_path, '..', 'print_config', 'cura_engine', 'materials')
if not os.path.exists(path): return []
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
materials = []
for f in files:
materials.append({'id': f, 'name': f.replace('.inst.cfg', '').replace('generic_', 'Generic ').replace('_', ' ').title()})
materials.sort(key=lambda x: x['name'])
return materials
except:
return []

View File

@@ -30,10 +30,21 @@ class PrusaSlicerEngine:
# Map quality, infill, supports to PrusaSlicer CLI arguments. # Map quality, infill, supports to PrusaSlicer CLI arguments.
# Example defaults, normally these would load from an .ini or be dynamically matched. # Example defaults, normally these would load from an .ini or be dynamically matched.
quality_preset = kwargs.get('quality_preset') quality_preset = kwargs.get('quality_preset')
material_preset = kwargs.get('material_preset')
infill_density = kwargs.get('infill_density') infill_density = kwargs.get('infill_density')
support_enable = kwargs.get('support_enable') support_enable = kwargs.get('support_enable')
support_pattern = kwargs.get('support_pattern') support_pattern = kwargs.get('support_pattern')
if quality_preset:
q_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'quality', f"{quality_preset}.ini")
if os.path.exists(q_ini):
command.extend(['--load', q_ini])
if material_preset:
m_ini = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'materials', f"{material_preset}.ini")
if os.path.exists(m_ini):
command.extend(['--load', m_ini])
if infill_density is not None: if infill_density is not None:
command.extend(["--fill-density", f"{infill_density}%"]) command.extend(["--fill-density", f"{infill_density}%"])
@@ -80,10 +91,29 @@ class PrusaSlicerEngine:
}) })
return quality_presets return quality_presets
def get_support_patterns(self): def get_support_patterns(self, app):
return [ all_files = [f for f in os.listdir(os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer',"supports")) if f.endswith('.ini')]
{'id': 'rectilinear', 'name': 'Rectilinear'}, support_presets = []
{'id': 'grid', 'name': 'Grid'}, for file in all_files:
{'id': 'organic', 'name': 'Organic (Tree)'}, with open(os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', "supports", file), 'r') as f:
{'id': 'snug', 'name': 'Snug'} config = configparser.ConfigParser()
] config.read_file(f)
if 'metadata' in config:
support_presets.append({
'id': file.replace('.ini', ''),
'name': config['metadata'].get('show_name', file.replace('.ini', '').replace('_', ' '))
})
return support_presets
def get_materials(self, app):
try:
path = os.path.join(app.root_path, '..', 'print_config', 'prusa_slicer', 'materials')
if not os.path.exists(path): return []
files = [f for f in os.listdir(path) if f.endswith('.ini')]
materials = []
for f in files:
materials.append({'id': f.replace('.ini', ''), 'name': f.replace('.ini', '').replace('_', ' ')})
materials.sort(key=lambda x: x['name'])
return materials
except:
return []

View File

@@ -26,7 +26,7 @@ def get_gcode_dir(app):
@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, material_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False):
# This is run by the Huey worker # This is run by the Huey worker
# We need to create an app context to interact with the database # We need to create an app context to interact with the database
from app import create_app from app import create_app
@@ -61,6 +61,7 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=No
stl_filepath=stl_filepath, stl_filepath=stl_filepath,
gcode_filepath=gcode_filepath, gcode_filepath=gcode_filepath,
quality_preset=quality_preset, quality_preset=quality_preset,
material_preset=material_preset,
infill_density=infill_density, infill_density=infill_density,
support_enable=support_enable, support_enable=support_enable,
support_pattern=support_pattern support_pattern=support_pattern
@@ -95,7 +96,7 @@ def slice_stl_task(file_id, stl_filepath, quality_preset=None, infill_density=No
@huey.task() @huey.task()
def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False): def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None, material_preset=None, infill_density=None, support_enable=None, support_pattern=None, delete_stl=False):
from app import create_app from app import create_app
app = create_app() app = create_app()
with app.app_context(): with app.app_context():
@@ -112,7 +113,7 @@ def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None,
# Now trigger the regular slicing task # Now trigger the regular slicing task
# We can just call the slicing logic or enqueue it # We can just call the slicing logic or enqueue it
slice_stl_task(file_id, merged_filepath, quality_preset, infill_density, support_enable, support_pattern, delete_stl=delete_stl) slice_stl_task(file_id, merged_filepath, quality_preset, material_preset, infill_density, support_enable, support_pattern, delete_stl=delete_stl)
except Exception as e: except Exception as e:
print_file = PrintFile.query.get(file_id) print_file = PrintFile.query.get(file_id)
if print_file: if print_file:

View File

@@ -0,0 +1,50 @@
[metadata]
show_name = Grid Support
[settings]
enable_support = 1
support_filament = 0
support_line_width = 0.4
support_interface_filament = 0
support_on_build_plate_only = 0
support_top_z_distance = 0.2
support_interface_loop_pattern = 0
support_interface_top_layers = 2
support_interface_spacing = 0.5
support_interface_speed = 100%
support_base_pattern = rectilinear
support_base_pattern_spacing = 2.5
support_speed = 50
support_threshold_angle = 30
support_object_xy_distance = 0.35
support_type = normal(auto)
support_style = Grid
support_interface_bottom_layers = 2
tree_support_branch_angle = 45
tree_support_wall_count = 0
support_angle = 0
support_bottom_interface_spacing = 0.5
support_bottom_z_distance = 0.2
support_critical_regions_only = 0
support_expansion = 0
support_interface_not_for_body = 1
support_interface_pattern = auto
support_remove_small_overhang = 1
support_xy_overrides_z = xy_overrides_z
tree_support_adaptive_layer_height = 1
tree_support_angle_slow = 25
tree_support_auto_brim = 1
tree_support_branch_angle_organic = 40
tree_support_branch_diameter = 2
tree_support_branch_diameter_angle = 5
tree_support_branch_diameter_double_wall = 3
tree_support_branch_diameter_organic = 2
tree_support_branch_distance = 5
tree_support_branch_distance_organic = 1
tree_support_brim_width = 3
tree_support_tip_diameter = 0.8
tree_support_top_rate = 30%

View File

@@ -0,0 +1,50 @@
[metadata]
show_name = Snug Support
[settings]
enable_support = 1
support_filament = 0
support_line_width = 0.4
support_interface_filament = 0
support_on_build_plate_only = 0
support_top_z_distance = 0.2
support_interface_loop_pattern = 0
support_interface_top_layers = 2
support_interface_spacing = 0.5
support_interface_speed = 100%
support_base_pattern = rectilinear
support_base_pattern_spacing = 2.5
support_speed = 50
support_threshold_angle = 30
support_object_xy_distance = 0.35
support_type = normal(auto)
support_style = Snug
support_interface_bottom_layers = 2
tree_support_branch_angle = 45
tree_support_wall_count = 0
support_angle = 0
support_bottom_interface_spacing = 0.5
support_bottom_z_distance = 0.2
support_critical_regions_only = 0
support_expansion = 0
support_interface_not_for_body = 1
support_interface_pattern = auto
support_remove_small_overhang = 1
support_xy_overrides_z = xy_overrides_z
tree_support_adaptive_layer_height = 1
tree_support_angle_slow = 25
tree_support_auto_brim = 1
tree_support_branch_angle_organic = 40
tree_support_branch_diameter = 2
tree_support_branch_diameter_angle = 5
tree_support_branch_diameter_double_wall = 3
tree_support_branch_diameter_organic = 2
tree_support_branch_distance = 5
tree_support_branch_distance_organic = 1
tree_support_brim_width = 3
tree_support_tip_diameter = 0.8
tree_support_top_rate = 30%

View File

@@ -0,0 +1,50 @@
[metadata]
show_name = Tree Support
[settings]
enable_support = 1
support_filament = 0
support_line_width = 0.4
support_interface_filament = 0
support_on_build_plate_only = 0
support_top_z_distance = 0.2
support_interface_loop_pattern = 0
support_interface_top_layers = 2
support_interface_spacing = 0.5
support_interface_speed = 100%
support_base_pattern = rectilinear
support_base_pattern_spacing = 2.5
support_speed = 50
support_threshold_angle = 30
support_object_xy_distance = 0.35
support_type = normal(auto)
support_style = Organic
support_interface_bottom_layers = 2
tree_support_branch_angle = 45
tree_support_wall_count = 0
support_angle = 0
support_bottom_interface_spacing = 0.5
support_bottom_z_distance = 0.2
support_critical_regions_only = 0
support_expansion = 0
support_interface_not_for_body = 1
support_interface_pattern = auto
support_remove_small_overhang = 1
support_xy_overrides_z = xy_overrides_z
tree_support_adaptive_layer_height = 1
tree_support_angle_slow = 25
tree_support_auto_brim = 1
tree_support_branch_angle_organic = 40
tree_support_branch_diameter = 2
tree_support_branch_diameter_angle = 5
tree_support_branch_diameter_double_wall = 3
tree_support_branch_diameter_organic = 2
tree_support_branch_distance = 5
tree_support_branch_distance_organic = 1
tree_support_brim_width = 3
tree_support_tip_diameter = 0.8
tree_support_top_rate = 30%