diff --git a/app/assets/i18n/de.json b/app/assets/i18n/de.json index 0375b55..4eff4d3 100644 --- a/app/assets/i18n/de.json +++ b/app/assets/i18n/de.json @@ -218,5 +218,12 @@ "Add User": "Benutzer hinzufügen", "Password": "Passwort", "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" } \ No newline at end of file diff --git a/app/assets/i18n/en.json b/app/assets/i18n/en.json index 9ddbacc..c381558 100644 --- a/app/assets/i18n/en.json +++ b/app/assets/i18n/en.json @@ -218,5 +218,12 @@ "Add User": "Add User", "Password": "Password", "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" } \ No newline at end of file diff --git a/app/assets/i18n/zh-cn.json b/app/assets/i18n/zh-cn.json index 0331a49..0a4d850 100644 --- a/app/assets/i18n/zh-cn.json +++ b/app/assets/i18n/zh-cn.json @@ -218,5 +218,12 @@ "Add User": "添加用户", "Password": "密码", "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": "材料配置" } \ No newline at end of file diff --git a/app/routes/admin_routes.py b/app/routes/admin_routes.py index de3fc8e..e084815 100644 --- a/app/routes/admin_routes.py +++ b/app/routes/admin_routes.py @@ -46,6 +46,7 @@ def settings(): 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') + default_material = request.form.get('default_material', '') gcode_upload_folder = request.form.get('gcode_upload_folder', '').strip() slicer_engine = request.form.get('slicer_engine', 'cura') build_plate_model_path = request.form.get('build_plate_model_path', '').strip() @@ -59,6 +60,7 @@ def settings(): ('default_support', default_support), ('default_support_pattern', default_support_pattern), ('default_quality', default_quality), + ('default_material', default_material), ('gcode_upload_folder', gcode_upload_folder), ('slicer_engine', slicer_engine), ('build_plate_model_path', build_plate_model_path), diff --git a/app/routes/main_routes.py b/app/routes/main_routes.py index 0d1373e..7f50b16 100644 --- a/app/routes/main_routes.py +++ b/app/routes/main_routes.py @@ -355,10 +355,11 @@ def plater(): 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') + 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() 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/') @login_required @@ -390,6 +391,7 @@ def merge_and_slice(): data = request.json pieces = data.get('pieces', []) quality = data.get('quality', 'base_global_standard.inst.cfg') + material = data.get('material', '') infill_density = data.get('infill', '20') support_enable = data.get('support', 'false') support_pattern = data.get('support_pattern', 'lines') @@ -427,6 +429,7 @@ def merge_and_slice(): "matrix": p['raw_matrix'], "settings": { "quality": quality, + "material": material, "infill": infill_density, "support": support_enable, "support_pattern": support_pattern @@ -450,6 +453,7 @@ def merge_and_slice(): "parts": [], "settings": { "quality": quality, + "material": material, "infill": infill_density, "support": support_enable, "support_pattern": support_pattern @@ -471,6 +475,7 @@ def merge_and_slice(): "matrix": pieces[0].get('raw_matrix', pieces[0]['matrix']), "settings": { "quality": quality, + "material": material, "infill": infill_density, "support": support_enable, "support_pattern": support_pattern @@ -482,7 +487,7 @@ def merge_and_slice(): temp_filename = f"temp_edit_{uuid.uuid4().hex}.stl" temp_filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], temp_filename) - merge_and_slice_task(target_file_id, inputs, temp_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=True) + 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: target_file_id = pieces[0]['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_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: # Multiple models, create a new "Merged Slice" PrintFile entry to keep track of combination timestamp = datetime.now().strftime('%Y%m%d%H%M%S') @@ -508,6 +513,7 @@ def merge_and_slice(): "parts": [], "settings": { "quality": quality, + "material": material, "infill": infill_density, "support": support_enable, "support_pattern": support_pattern @@ -534,7 +540,7 @@ def merge_and_slice(): db.session.add(print_file) db.session.commit() - merge_and_slice_task(print_file.id, inputs, merged_filepath, quality, infill_density, support_enable, support_pattern, delete_stl=False) + 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!'}) @@ -553,5 +559,6 @@ def engine_options(engine_name): from app.utils.slice_engines import get_slicer_engine engine = get_slicer_engine(engine_name) presets = engine.get_quality_presets(current_app) - patterns = engine.get_support_patterns() - return jsonify({'presets': presets, 'support_patterns': patterns}) + patterns = engine.get_support_patterns(current_app) + materials = engine.get_materials(current_app) if hasattr(engine, 'get_materials') else [] + return jsonify({'presets': presets, 'support_patterns': patterns, 'materials': materials}) diff --git a/app/templates/admin/settings.html b/app/templates/admin/settings.html index f59b086..4ca13f3 100644 --- a/app/templates/admin/settings.html +++ b/app/templates/admin/settings.html @@ -12,25 +12,25 @@
-
{{ _('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.') }}
-
{{ _('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.') }}
-
{{ _('Files smaller than this will not generate a simplified proxy.') }}
+
{{ _('Files smaller than this will not generate a simplified proxy.') }}
-
{{ _('Absolute path to save locally sliced GCode files (e.g. OctoPrint uploads folder like "/home/pi/.octoprint/uploads"). Leave empty to use system default.') }}
+
{{ _('Absolute path to save locally sliced GCode files (e.g. OctoPrint uploads folder like "/home/pi/.octoprint/uploads"). Leave empty to use system default.') }}
{{ _('Default Plater Settings') }}
@@ -38,7 +38,7 @@
-
{{ _('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.') }}
@@ -62,13 +62,20 @@
-
+
+
+ + +
+
{{ _('Slicing Engine Configurations') }}
@@ -151,6 +158,7 @@ function submitSettings(event) { document.addEventListener('DOMContentLoaded', function() { const engineSelect = document.getElementById('slicer_engine'); const qualitySelect = document.getElementById('default_quality'); + const materialSelect = document.getElementById('default_material'); const patternSelect = document.getElementById('default_support_pattern'); function updateOptions(engine) { @@ -166,6 +174,21 @@ document.addEventListener('DOMContentLoaded', function() { }); 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'); patternSelect.innerHTML = ''; data.support_patterns.forEach(p => { diff --git a/app/templates/base.html b/app/templates/base.html index a933b32..2a3198e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -65,6 +65,7 @@
diff --git a/app/templates/slice/gcode_preview.html b/app/templates/slice/gcode_preview.html index 77b1cc8..35b5a56 100644 --- a/app/templates/slice/gcode_preview.html +++ b/app/templates/slice/gcode_preview.html @@ -66,8 +66,25 @@ document.addEventListener('DOMContentLoaded', async function() { 'SKIRT': new THREE.Color(0x00ffff), 'SUPPORT-INTERFACE': new THREE.Color(0x2b6b2b), '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 const bedWidth = {{ machine_width | default(220) }}; @@ -83,6 +100,23 @@ document.addEventListener('DOMContentLoaded', async function() { '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 scene, camera, renderer, controls; let group = new THREE.Group(); @@ -159,6 +193,21 @@ document.addEventListener('DOMContentLoaded', async function() { 'SUPPORT-INTERFACE': 'uShowSupportInterface', '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 => { el.addEventListener('click', function() { @@ -292,16 +341,29 @@ document.addEventListener('DOMContentLoaded', async function() { } for (let i = 0; i < lines.length; i++) { - let chunk = lines[i].trim().toUpperCase(); + let chunk = lines[i].trim(); if (!chunk) continue; + let upperChunk = chunk.toUpperCase(); - if (chunk.startsWith(';LAYER:')) { + if (upperChunk.startsWith(';LAYER:')) { flushLayer(); - } else if (chunk.startsWith(';TYPE:')) { + } else if (upperChunk.startsWith(';TYPE:')) { 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 parts = chunk.split(/\s+/); + let parts = upperChunk.split(/\s+/); let hasMove = false; 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)) { let isExtrude = (next.e > current.e); // 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 col = COLORS[activeType] || COLORS['DEFAULT']; - let tIdx = TYPE_INDEX[activeType] !== undefined ? TYPE_INDEX[activeType] : TYPE_INDEX['DEFAULT']; + // Special case for default aliases like "; External perimeter" which we stored in currentTypeStr + 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) { let dx = next.x - current.x; diff --git a/app/templates/slice/plater.html b/app/templates/slice/plater.html index 4611f9a..b9778e0 100644 --- a/app/templates/slice/plater.html +++ b/app/templates/slice/plater.html @@ -101,6 +101,20 @@
+
+ +
+
+
+ +
+
+
+
+